(function ($) {

AblePlayer.prototype.initYouTubePlayer = function () {

        var thisObj, deferred, promise, youTubeId, googleApiPromise, json;
        thisObj = this;

        deferred = new $.Deferred();
        promise = deferred.promise();

        // if a described version is available && user prefers desription
        // init player using the described version
        if (this.youTubeDescId && this.prefDesc) {
                youTubeId = this.youTubeDescId;
        }
        else {
                youTubeId = this.youTubeId;
        }
        this.activeYouTubeId = youTubeId;
        if (AblePlayer.youtubeIframeAPIReady) {
                // Script already loaded and ready.
                this.finalizeYoutubeInit().then(function() {
                        deferred.resolve();
                });
        }
        else {
                // Has another player already started loading the script? If so, abort...
                if (!AblePlayer.loadingYoutubeIframeAPI) {
                        $.getScript('https://www.youtube.com/iframe_api').fail(function () {
                                deferred.fail();
                        });
                }

                // Otherwise, keeping waiting for script load event...
                $('body').on('youtubeIframeAPIReady', function () {
                        thisObj.finalizeYoutubeInit().then(function() {
                                deferred.resolve();
                        });
                });
        }
        return promise;
};

AblePlayer.prototype.finalizeYoutubeInit = function () {

        // This is called once we're sure the Youtube iFrame API is loaded -- see above
        var deferred, promise, thisObj, containerId, ccLoadPolicy, videoDimensions, autoplay;

        deferred = new $.Deferred();
        promise = deferred.promise();

        thisObj = this;

        containerId = this.mediaId + '_youtube';

        this.$mediaContainer.prepend($('<div>').attr('id', containerId));
        // NOTE: Tried the following in place of the above in January 2016
        // because in some cases two videos were being added to the DOM
        // However, once v2.2.23 was fairly stable, unable to reproduce that problem
        // so maybe it's not an issue. This is preserved here temporarily, just in case it's needed...
        // thisObj.$mediaContainer.html($('<div>').attr('id', containerId));

        // cc_load_policy:
        // 0 - show captions depending on user's preference on YouTube
        // 1 - show captions by default, even if the user has turned them off
        // For Able Player, init player with value of 0
        // and will turn them on or off after player is initialized
        // based on availability of local tracks and user's Able Player prefs
        ccLoadPolicy = 0;

        videoDimensions = this.getYouTubeDimensions(this.activeYouTubeId, containerId);
        if (videoDimensions) {
                this.ytWidth = videoDimensions[0];
                this.ytHeight = videoDimensions[1];
                this.aspectRatio = thisObj.ytWidth / thisObj.ytHeight;
        }
        else {
                // dimensions are initially unknown
                // sending null values to YouTube results in a video that uses the default YouTube dimensions
                // these can then be scraped from the iframe and applied to this.$ableWrapper
                this.ytWidth = null;
                this.ytHeight = null;
        }

        if (this.okToPlay) {
                autoplay = 1;
        }
        else {
                autoplay = 0;
        }

        // NOTE: YouTube is changing the following parameters on or after Sep 25, 2018:
        // rel - No longer able to prevent YouTube from showing related videos
        //                      value of 0 now limits related videos to video's same channel
        // showinfo - No longer supported (previously, value of 0 hid title, share, & watch later buttons
        // Documentation https://developers.google.com/youtube/player_parameters

        this.youTubePlayer = new YT.Player(containerId, {
                videoId: this.activeYouTubeId,
                host: this.youTubeNoCookie ? 'https://www.youtube-nocookie.com' : 'https://www.youtube.com',
                width: this.ytWidth,
                height: this.ytHeight,
                playerVars: {
                        autoplay: autoplay,
                        enablejsapi: 1,
                        disableKb: 1, // disable keyboard shortcuts, using our own
                        playsinline: this.playsInline,
                        start: this.startTime,
                        controls: 0, // no controls, using our own
                        cc_load_policy: ccLoadPolicy,
                        hl: this.lang, // use the default language UI
                        modestbranding: 1, // no YouTube logo in controller
                        rel: 0, // do not show related videos when video ends
                        html5: 1, // force html5 if browser supports it (undocumented parameter; 0 does NOT force Flash)
                        iv_load_policy: 3 // do not show video annotations
                },
                events: {
                        onReady: function () {
                                if (thisObj.swappingSrc) {
                                        // swap is now complete
                                        thisObj.swappingSrc = false;
                                        thisObj.cueingPlaylistItem = false;
                                        if (thisObj.playing) {
                                                // resume playing
                                                thisObj.playMedia();
                                        }
                                }
                                if (thisObj.userClickedPlaylist) {
                                        thisObj.userClickedPlaylist = false; // reset
                                }
                                if (typeof thisObj.aspectRatio === 'undefined') {
                                        thisObj.resizeYouTubePlayer(thisObj.activeYouTubeId, containerId);
                                }
                                deferred.resolve();
                        },
                        onError: function (x) {
                                deferred.fail();
                        },
                        onStateChange: function (x) {
                                thisObj.getPlayerState().then(function(playerState) {
                                        // values of playerState: 'playing','paused','buffering','ended'
                                        if (playerState === 'playing') {
                                                thisObj.playing = true;
                                                thisObj.startedPlaying = true;
                                                thisObj.paused = false;
                                        }
                                        else if (playerState == 'ended') {
                                                thisObj.onMediaComplete();
                                        }
                                        else {
                                                thisObj.playing = false;
                                                thisObj.paused = true;
                                        }
                                        if (thisObj.stoppingYouTube && playerState === 'paused') {
                                                if (typeof thisObj.$posterImg !== 'undefined') {
                                                        thisObj.$posterImg.show();
                                                }
                                                thisObj.stoppingYouTube = false;
                                                thisObj.seeking = false;
                                                thisObj.playing = false;
                                                thisObj.paused = true;
                                        }
                                });
                        },
                        onPlaybackQualityChange: function () {
                                // do something
                        },
                        onApiChange: function (x) {
                                // As of Able Player v2.2.23, we are now getting caption data via the YouTube Data API
                                // prior to calling initYouTubePlayer()
                                // Previously we got caption data via the YouTube iFrame API, and doing so was an awful mess.
                                // onApiChange fires to indicate that the player has loaded (or unloaded) a module with exposed API methods
                                // it isn't fired until the video starts playing
                                // if captions are available for this video (automated captions don't count)
                                // the 'captions' (or 'cc') module is loaded. If no captions are available, this event never fires
                                // So, to trigger this event we had to play the video briefly, then pause, then reset.
                                // During that brief moment of playback, the onApiChange event was fired and we could setup captions
                                // The 'captions' and 'cc' modules are very different, and have different data and methods
                                // NOW, in v2.2.23, we still need to initialize the caption modules in order to control captions
                                // but we don't have to do that on load in order to get caption data
                                // Instead, we can wait until the video starts playing normally, then retrieve the modules
                                thisObj.initYouTubeCaptionModule();
                        }
                }
        });

        this.injectPoster(this.$mediaContainer, 'youtube');
        if (!this.hasPlaylist) {
                // remove the media element, since YouTube replaces that with its own element in an iframe
                // this is handled differently for playlists. See buildplayer.js > cuePlaylistItem()
                this.$media.remove();
        }
        return promise;
};

AblePlayer.prototype.getYouTubeDimensions = function (youTubeContainerId) {

        // get dimensions of YouTube video, return array with width & height
        // Sources, in order of priority:
        // 1. The width and height attributes on <video>
        // 2. YouTube (not yet supported; can't seem to get this data via YouTube Data API without OAuth!)

        var d, url, $iframe, width, height;

        d = [];

        if (typeof this.playerMaxWidth !== 'undefined') {
                d[0] = this.playerMaxWidth;
                // optional: set height as well; not required though since YouTube will adjust height to match width
                if (typeof this.playerMaxHeight !== 'undefined') {
                        d[1] = this.playerMaxHeight;
                }
                return d;
        }
        else {
                if (typeof $('#' + youTubeContainerId) !== 'undefined') {
                        $iframe = $('#' + youTubeContainerId);
                        width = $iframe.width();
                        height = $iframe.height();
                        if (width > 0 && height > 0) {
                                d[0] = width;
                                d[1] = height;
                                return d;
                        }
                }
        }
        return false;
};

AblePlayer.prototype.resizeYouTubePlayer = function(youTubeId, youTubeContainerId) {

        // called after player is ready, if youTube dimensions were previously unknown
        // Now need to get them from the iframe element that YouTube injected
        // and resize Able Player to match
        var d, width, height;
        if (typeof this.aspectRatio !== 'undefined') {
                // video dimensions have already been collected
                if (this.restoringAfterFullScreen) {
                        // restore using saved values
                        if (this.youTubePlayer) {
                                this.youTubePlayer.setSize(this.ytWidth, this.ytHeight);
                        }
                        this.restoringAfterFullScreen = false;
                }
                else {
                        // recalculate with new wrapper size
                        width = this.$ableWrapper.parent().width();
                        height = Math.round(width / this.aspectRatio);
                        this.$ableWrapper.css({
                                'max-width': width + 'px',
                                'width': ''
                        });
                        this.youTubePlayer.setSize(width, height);
                        if (this.fullscreen) {
                                this.youTubePlayer.setSize(width, height);
                        }
                        else {
                                // resizing due to a change in window size, not full screen
                                this.youTubePlayer.setSize(this.ytWidth, this.ytHeight);
                        }
                }
        }
        else {
                d = this.getYouTubeDimensions(youTubeContainerId);
                if (d) {
                        width = d[0];
                        height = d[1];
                        if (width > 0 && height > 0) {
                                this.aspectRatio = width / height;
                                this.ytWidth = width;
                                this.ytHeight = height;
                                if (width !== this.$ableWrapper.width()) {
                                        // now that we've retrieved YouTube's default width,
                                        // need to adjust to fit the current player wrapper
                                        width = this.$ableWrapper.width();
                                        height = Math.round(width / this.aspectRatio);
                                        if (this.youTubePlayer) {
                                                this.youTubePlayer.setSize(width, height);
                                        }
                                }
                        }
                }
        }
};

AblePlayer.prototype.setupYouTubeCaptions = function () {

        // called from setupAltCaptions if player is YouTube and there are no <track> captions

        // use YouTube Data API to get caption data from YouTube
        // function is called only if these conditions are met:
        // 1. this.player === 'youtube'
        // 2. there are no <track> elements with kind="captions"
        // 3. youTubeDataApiKey is defined

        var deferred = new $.Deferred();
        var promise = deferred.promise();

        var thisObj, googleApiPromise, youTubeId, i;

        thisObj = this;

        // if a described version is available && user prefers desription
        // Use the described version, and get its captions
        if (this.youTubeDescId && this.prefDesc) {
                youTubeId = this.youTubeDescId;
        }
        else {
                youTubeId = this.youTubeId;
        }

        if (typeof youTubeDataAPIKey !== 'undefined') {
                // Wait until Google Client API is loaded
                // When loaded, it sets global var googleApiReady to true

                // Thanks to Paul Tavares for $.doWhen()
                // https://gist.github.com/purtuga/8257269
                $.doWhen({
                        when: function(){
                                return googleApiReady;
                        },
                        interval: 100, // ms
                        attempts: 1000
                })
                .done(function(){
                                deferred.resolve();
                })
                .fail(function(){
                        console.log('Unable to initialize Google API. YouTube captions are currently unavailable.');
                });
        }
        else {
                deferred.resolve();
        }
        return promise;
};

AblePlayer.prototype.waitForGapi = function () {

        // wait for Google API to initialize

        var thisObj, deferred, promise, maxWaitTime, maxTries, tries, timer, interval;

        thisObj = this;
        deferred = new $.Deferred();
        promise = deferred.promise();
        maxWaitTime = 5000; // 5 seconds
        maxTries = 100; // number of tries during maxWaitTime
        tries = 0;
        interval = Math.floor(maxWaitTime/maxTries);

        timer = setInterval(function() {
                tries++;
                if (googleApiReady || tries >= maxTries) {
                        clearInterval(timer);
                        if (googleApiReady) { // success!
                                deferred.resolve(true);
                        }
                        else { // tired of waiting
                                deferred.resolve(false);
                        }
                }
                else {
                        thisObj.waitForGapi();
                }
        }, interval);
        return promise;
};

AblePlayer.prototype.getYouTubeCaptionTracks = function (youTubeId) {

        // get data via YouTube Data API, and push data to this.captions
        var deferred = new $.Deferred();
        var promise = deferred.promise();

        var thisObj, useGoogleApi, i, trackId, trackLang, trackName, trackLabel, trackKind, isDraft, isDefaultTrack;

        thisObj = this;

        if (typeof youTubeDataAPIKey !== 'undefined') {
                this.waitForGapi().then(function(waitResult) {

                        useGoogleApi = waitResult;

                        // useGoogleApi returns false if API failed to initalize after max wait time
                        // Proceed only if true. Otherwise can still use fallback method (see else loop below)
                        if (useGoogleApi === true) {
                                gapi.client.setApiKey(youTubeDataAPIKey);
                                gapi.client
                                        .load('youtube', 'v3')
                                        .then(function() {
                                                var request = gapi.client.youtube.captions.list({
                                                        'part': 'id, snippet',
                                                        'videoId': youTubeId
                                                });
                                                request.then(function(json) {
                                                        if (json.result.items.length) { // video has captions!
                                                                thisObj.hasCaptions = true;
                                                                thisObj.usingYouTubeCaptions = true;
                                                                if (thisObj.prefCaptions === 1) {
                                                                        thisObj.captionsOn = true;
                                                                }
                                                                else {
                                                                        thisObj.captionsOn = false;
                                                                }
                                                                // Step through results and add them to cues array
                                                                for (i=0; i < json.result.items.length; i++) {
                                                                        trackName = json.result.items[i].snippet.name; // usually seems to be empty
                                                                        trackLang = json.result.items[i].snippet.language;
                                                                        trackKind = json.result.items[i].snippet.trackKind; // ASR, standard, forced
                                                                        isDraft = json.result.items[i].snippet.isDraft; // Boolean
                                                                        // Other variables that could potentially be collected from snippet:
                                                                        // isCC - Boolean, always seems to be false
                                                                        // isLarge - Boolean
                                                                        // isEasyReader - Boolean
                                                                        // isAutoSynced  Boolean
                                                                        // status - string, always seems to be "serving"

                                                                        var srcUrl = thisObj.getYouTubeTimedTextUrl(youTubeId,trackName,trackLang);
                                                                        if (trackKind !== 'ASR' && !isDraft) {

                                                                                if (trackName !== '') {
                                                                                         trackLabel = trackName;
                                                                                }
                                                                                else {
                                                                                         // if track name is empty (it always seems to be), assign a label based on trackLang
                                                                                         trackLabel = thisObj.getLanguageName(trackLang);
                                                                                }

                                                                                // assign the default track based on language of the player
                                                                                if (trackLang === thisObj.lang) {
                                                                                        isDefaultTrack = true;
                                                                                }
                                                                                else {
                                                                                        isDefaultTrack = false;
                                                                                }
                                                                                thisObj.tracks.push({
                                                                                        'kind': 'captions',
                                                                                        'src': srcUrl,
                                                                                        'language': trackLang,
                                                                                        'label': trackLabel,
                                                                                        'def': isDefaultTrack
                                                                                });
                                                                        }
                                                                }
                                                                // setupPopups again with new captions array, replacing original
                                                                thisObj.setupPopups('captions');
                                                                deferred.resolve();
                                                        }
                                                        else {
                                                                thisObj.hasCaptions = false;
                                                                thisObj.usingYouTubeCaptions = false;
                                                                deferred.resolve();
                                                        }
                                                }, function (reason) {
                                                        // If video has no captions, YouTube returns an error.
                                                        // Should still proceed, but with captions disabled
                                                        // The specific error, if needed: reason.result.error.message
                                                        // If no captions, the error is: "The video identified by the <code>videoId</code> parameter could not be found."
                                                        console.log('Error retrieving captions.');
                                                        console.log('Check your video on YouTube to be sure captions are available and published.');
                                                        thisObj.hasCaptions = false;
                                                        thisObj.usingYouTubeCaptions = false;
                                                        deferred.resolve();
                                                });
                                        })
                        }
                        else {
                                // googleAPi never loaded.
                                this.getYouTubeCaptionTracks2(youTubeId).then(function() {
                                        deferred.resolve();
                                });
                        }
                });
        }
        else {
                // web owner hasn't provided a Google API key
                // attempt to get YouTube captions via the backup method
                this.getYouTubeCaptionTracks2(youTubeId).then(function() {
                        deferred.resolve();
                });
        }
        return promise;
};

AblePlayer.prototype.getYouTubeCaptionTracks2 = function (youTubeId) {

        // Use alternative backup method of getting caption tracks from YouTube
        // and pushing them to this.captions
        // Called from getYouTubeCaptionTracks if no Google API key is defined
        // or if Google API failed to initiatlize
        // This method seems to be undocumented, but is referenced on StackOverflow
        // We'll use that as a fallback but it could break at any moment

        var deferred = new $.Deferred();
        var promise = deferred.promise();

        var thisObj, useGoogleApi, i, trackId, trackLang, trackName, trackLabel, trackKind, isDraft, isDefaultTrack;

        thisObj = this;

        $.ajax({
                type: 'get',
                url: 'https://www.youtube.com/api/timedtext?type=list&v=' + youTubeId,
                dataType: 'xml',
                success: function(xml) {
                        var $tracks = $(xml).find('track');
                        if ($tracks.length > 0) {        // video has captions!
                                thisObj.hasCaptions = true;
                                thisObj.usingYouTubeCaptions = true;
                                if (thisObj.prefCaptions === 1) {
                                        thisObj.captionsOn = true;
                                }
                                else {
                                        thisObj.captionsOn = false;
                                }
                                // Step through results and add them to tracks array
                                $tracks.each(function() {
                                        trackId = $(this).attr('id');
                                        trackLang = $(this).attr('lang_code');
                                        if ($(this).attr('name') !== '') {
                                                trackName = $(this).attr('name');
                                                trackLabel = trackName;
                                        }
                                        else {
                                                // @name is typically null except for default track
                                                // but lang_translated seems to be reliable
                                                trackName = '';
                                                trackLabel = $(this).attr('lang_translated');
                                        }
                                        if (trackLabel === '') {
                                                trackLabel = thisObj.getLanguageName(trackLang);
                                        }
                                        // assign the default track based on language of the player
                                        if (trackLang === thisObj.lang) {
                                                isDefaultTrack = true;
                                        }
                                        else {
                                                isDefaultTrack = false;
                                        }

                                        // Build URL for retrieving WebVTT source via YouTube's timedtext API
                                        var srcUrl = thisObj.getYouTubeTimedTextUrl(youTubeId,trackName,trackLang);
                                        thisObj.tracks.push({
                                                'kind': 'captions',
                                                'src': srcUrl,
                                                'language': trackLang,
                                                'label': trackLabel,
                                                'def': isDefaultTrack
                                        });

                                });
                                // setupPopups again with new captions array, replacing original
                                thisObj.setupPopups('captions');
                                deferred.resolve();
                        }
                        else {
                                thisObj.hasCaptions = false;
                                thisObj.usingYouTubeCaptions = false;
                                deferred.resolve();
                        }
                },
                error: function(xhr, status) {
                        console.log('Error retrieving YouTube caption data for video ' + youTubeId);
                        deferred.resolve();
                }
        });
        return promise;
};

AblePlayer.prototype.getYouTubeTimedTextUrl = function (youTubeId, trackName, trackLang) {

        // return URL for retrieving WebVTT source via YouTube's timedtext API
        // Note: This API seems to be undocumented, and could break anytime
        var url = 'https://www.youtube.com/api/timedtext?fmt=vtt';
        url += '&v=' + youTubeId;
        url += '&lang=' + trackLang;
        // if track has a value in the name field, it's *required* in the URL
        if (trackName !== '') {
                url += '&name=' + trackName;
        }
        return url;
};

AblePlayer.prototype.getYouTubeCaptionCues = function (youTubeId) {

        var deferred, promise, thisObj;

        var deferred = new $.Deferred();
        var promise = deferred.promise();

        thisObj = this;

        this.tracks = [];
        this.tracks.push({
                'kind': 'captions',
                'src': 'some_file.vtt',
                'language': 'en',
                'label': 'Fake English captions'
        });

        deferred.resolve();
        return promise;
};

AblePlayer.prototype.initYouTubeCaptionModule = function () {

        // This function is called when YouTube onApiChange event fires
        // to indicate that the player has loaded (or unloaded) a module with exposed API methods
        // it isn't fired until the video starts playing
        // and only fires if captions are available for this video (automated captions don't count)
        // If no captions are available, onApichange event never fires & this function is never called

        // YouTube iFrame API documentation is incomplete related to captions
        // Found undocumented features on user forums and by playing around
        // Details are here: http://terrillthompson.com/blog/648
        // Summary:
        // User might get either the AS3 (Flash) or HTML5 YouTube player
        // The API uses a different caption module for each player (AS3 = 'cc'; HTML5 = 'captions')
        // There are differences in the data and methods available through these modules
        // This function therefore is used to determine which captions module is being used
        // If it's a known module, this.ytCaptionModule will be used elsewhere to control captions
        var options, fontSize, displaySettings;

        options = this.youTubePlayer.getOptions();
        if (options.length) {
                for (var i=0; i<options.length; i++) {
                        if (options[i] == 'cc') { // this is the AS3 (Flash) player
                                this.ytCaptionModule = 'cc';
                                if (!this.hasCaptions) {
                                        // there are captions available via other sources (e.g., <track>)
                                        // so use these
                                        this.hasCaptions = true;
                                        this.usingYouTubeCaptions = true;
                                }
                                break;
                        }
                        else if (options[i] == 'captions') { // this is the HTML5 player
                                this.ytCaptionModule = 'captions';
                                if (!this.hasCaptions) {
                                        // there are captions available via other sources (e.g., <track>)
                                        // so use these
                                        this.hasCaptions = true;
                                        this.usingYouTubeCaptions = true;
                                }
                                break;
                        }
                }
                if (typeof this.ytCaptionModule !== 'undefined') {
                        if (this.usingYouTubeCaptions) {
                                // set default languaage
                                this.youTubePlayer.setOption(this.ytCaptionModule, 'track', {'languageCode': this.captionLang});
                                // set font size using Able Player prefs (values are -1, 0, 1, 2, and 3, where 0 is default)
                                this.youTubePlayer.setOption(this.ytCaptionModule,'fontSize',this.translatePrefs('size',this.prefCaptionsSize,'youtube'));
                                // ideally could set other display options too, but no others seem to be supported by setOption()
                        }
                        else {
                                // now that we know which cc module was loaded, unload it!
                                // we don't want it if we're using local <track> elements for captions
                                this.youTubePlayer.unloadModule(this.ytCaptionModule)
                        }
                }
        }
        else {
                // no modules were loaded onApiChange
                // unfortunately, gonna have to disable captions if we can't control them
                this.hasCaptions = false;
                this.usingYouTubeCaptions = false;
        }
        this.refreshControls('captions');
};

AblePlayer.prototype.getYouTubePosterUrl = function (youTubeId, width) {

                 // return a URL for retrieving a YouTube poster image
                 // supported values of width: 120, 320, 480, 640

                 var url = 'https://img.youtube.com/vi/' + youTubeId;
                 if (width == '120') {
                         // default (small) thumbnail, 120 x 90
                         return url + '/default.jpg';
                 }
                 else if (width == '320') {
                         // medium quality thumbnail, 320 x 180
                         return url + '/hqdefault.jpg';
                 }
                 else if (width == '480') {
                         // high quality thumbnail, 480 x 360
                         return url + '/hqdefault.jpg';
                 }
                 else if (width == '640') {
                         // standard definition poster image, 640 x 480
                         return url + '/sddefault.jpg';
                 }
                 return false;
};

})(jQuery);