(function ($) {

AblePlayer.prototype.setupTranscript = function() {

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

        if (!this.transcriptType) {
                // previously set transcriptType to null since there are no <track> elements
                // check again to see if captions have been collected from other sources (e.g., YouTube)
                if (this.captions.length && (!(this.usingYouTubeCaptions || this.usingVimeoCaptions))) {
                        // captions are possible! Use the default type (popup)
                        // if other types ('external' and 'manual') were desired, transcriptType would not be null here
                        this.transcriptType = 'popup';
                }
        }

        if (this.transcriptType) {
                if (this.transcriptType === 'popup' || this.transcriptType === 'external') {
                         this.injectTranscriptArea();
                                deferred.resolve();
                }
                else if (this.transcriptType === 'manual') {
                        this.setupManualTranscript();
                        deferred.resolve();
                }
        }
        else {
                // there is no transcript
                deferred.resolve();
        }
        return promise;
};

AblePlayer.prototype.injectTranscriptArea = function() {

        var thisObj, $autoScrollLabel, $languageSelectWrapper, $languageSelectLabel, i, $option;

        thisObj = this;
        this.$transcriptArea = $('<div>', {
                'class': 'able-transcript-area',
                'tabindex': '-1'
        });

        this.$transcriptToolbar = $('<div>', {
                'class': 'able-window-toolbar able-' + this.toolbarIconColor + '-controls'
        });

        this.$transcriptDiv = $('<div>', {
                'class' : 'able-transcript'
        });

        // Transcript toolbar content

        // Add auto Scroll checkbox
        this.$autoScrollTranscriptCheckbox = $('<input>', {
                'id': 'autoscroll-transcript-checkbox',
                'type': 'checkbox'
         });
        $autoScrollLabel = $('<label>', {
                 'for': 'autoscroll-transcript-checkbox'
                }).text(this.tt.autoScroll);
        this.$transcriptToolbar.append($autoScrollLabel,this.$autoScrollTranscriptCheckbox);

        // Add field for selecting a transcript language
        // Only necessary if there is more than one language
        if (this.captions.length > 1) {
                $languageSelectWrapper = $('<div>',{
                        'class': 'transcript-language-select-wrapper'
                });
                $languageSelectLabel = $('<label>',{
                        'for': 'transcript-language-select'
                }).text(this.tt.language);
                this.$transcriptLanguageSelect = $('<select>',{
                        'id': 'transcript-language-select'
                });
                for (i=0; i < this.captions.length; i++) {
                        $option = $('<option></option>',{
                                value: this.captions[i]['language'],
                                lang: this.captions[i]['language']
                        }).text(this.captions[i]['label']);
                        if (this.captions[i]['def']) {
                                $option.prop('selected',true);
                         }
                        this.$transcriptLanguageSelect.append($option);
                 }
        }
        if ($languageSelectWrapper) {
                $languageSelectWrapper.append($languageSelectLabel,this.$transcriptLanguageSelect);
                this.$transcriptToolbar.append($languageSelectWrapper);
        }
        this.$transcriptArea.append(this.$transcriptToolbar, this.$transcriptDiv);

        // If client has provided separate transcript location, put it there.
        // Otherwise append it to the body
        if (this.transcriptDivLocation) {
                $('#' + this.transcriptDivLocation).append(this.$transcriptArea);
        }
        else {
                this.$ableWrapper.append(this.$transcriptArea);
        }

        // make it draggable (popup only; NOT external transcript)
        if (!this.transcriptDivLocation) {
                this.initDragDrop('transcript');
                if (this.prefTranscript === 1) {
                        // transcript is on. Go ahead and position it
                        this.positionDraggableWindow('transcript',this.getDefaultWidth('transcript'));
                }
        }

        // If client has provided separate transcript location, override user's preference for hiding transcript
        if (!this.prefTranscript && !this.transcriptDivLocation) {
                this.$transcriptArea.hide();
        }
};

AblePlayer.prototype.addTranscriptAreaEvents = function() {

        var thisObj = this;

        this.$autoScrollTranscriptCheckbox.click(function () {
                thisObj.handleTranscriptLockToggle(thisObj.$autoScrollTranscriptCheckbox.prop('checked'));
        });

        this.$transcriptDiv.on('mousewheel DOMMouseScroll click scroll', function (e) {
                // Propagation is stopped in transcript click handler, so clicks are on the scrollbar
                // or outside of a clickable span.
                if (!thisObj.scrollingTranscript) {
                        thisObj.autoScrollTranscript = false;
                        thisObj.refreshControls('transcript');
                }
                thisObj.scrollingTranscript = false;
        });

        if (typeof this.$transcriptLanguageSelect !== 'undefined') {

                this.$transcriptLanguageSelect.on('click mousedown',function (e) {
                        // execute default behavior
                        // prevent propagation of mouse event to toolbar or window
                        e.stopPropagation();
                });

                this.$transcriptLanguageSelect.on('change',function () {

                        var language = thisObj.$transcriptLanguageSelect.val();

                        thisObj.syncTrackLanguages('transcript',language);
                });
        }
};

AblePlayer.prototype.transcriptSrcHasRequiredParts = function() {

        // check the external transcript to be sure it has all required components
        // return true or false
        // in the process, define all the needed variables and properties

        if ($('#' + this.transcriptSrc).length) {
                this.$transcriptArea = $('#' + this.transcriptSrc);
                if (this.$transcriptArea.find('.able-window-toolbar').length) {
                        this.$transcriptToolbar = this.$transcriptArea.find('.able-window-toolbar').eq(0);
                        if (this.$transcriptArea.find('.able-transcript').length) {
                                this.$transcriptDiv = this.$transcriptArea.find('.able-transcript').eq(0);
                                if (this.$transcriptArea.find('.able-transcript-seekpoint').length) {
                                        this.$transcriptSeekpoints = this.$transcriptArea.find('.able-transcript-seekpoint');
                                        return true;
                                }
                        }
                }
        }
        return false;
}

AblePlayer.prototype.setupManualTranscript = function() {

        // Add an auto-scroll checkbox to the toolbar

        this.$autoScrollTranscriptCheckbox = $('<input id="autoscroll-transcript-checkbox" type="checkbox">');
        this.$transcriptToolbar.append($('<label for="autoscroll-transcript-checkbox">' + this.tt.autoScroll + ': </label>'), this.$autoScrollTranscriptCheckbox);

};

AblePlayer.prototype.updateTranscript = function() {

        if (!this.transcriptType) {
                return;
        }

        if (this.transcriptType === 'external' || this.transcriptType === 'popup') {

                var chapters, captions, descriptions;

                // Language of transcript might be different than language of captions
                // But both are in sync by default
                if (this.transcriptLang) {
                        captions = this.transcriptCaptions.cues;
                }
                else {
                        if (this.transcriptCaptions) {
                                this.transcriptLang = this.transcriptCaptions.language;
                                captions = this.transcriptCaptions.cues;
                        }
                        else if (this.selectedCaptions) {
                                this.transcriptLang = this.captionLang;
                                captions = this.selectedCaptions.cues;
                        }
                }

                // setup chapters
                if (this.transcriptChapters) {
                        chapters = this.transcriptChapters.cues;
                }
                else if (this.chapters.length > 0) {
                        // Try and match the caption language.
                        if (this.transcriptLang) {
                                for (var i = 0; i < this.chapters.length; i++) {
                                        if (this.chapters[i].language === this.transcriptLang) {
                                                chapters = this.chapters[i].cues;
                                        }
                                }
                        }
                        if (typeof chapters === 'undefined') {
                                chapters = this.chapters[0].cues || [];
                        }
                }

                // setup descriptions
                if (this.transcriptDescriptions) {
                        descriptions = this.transcriptDescriptions.cues;
                }
                else if (this.descriptions.length > 0) {
                        // Try and match the caption language.
                        if (this.transcriptLang) {
                                for (var i = 0; i < this.descriptions.length; i++) {
                                        if (this.descriptions[i].language === this.transcriptLang) {
                                                descriptions = this.descriptions[i].cues;
                                        }
                                }
                        }
                        if (!descriptions) {
                                descriptions = this.descriptions[0].cues || [];
                        }
                }

                var div = this.generateTranscript(chapters || [], captions || [], descriptions || []);

                this.$transcriptDiv.html(div);
                // reset transcript selected <option> to this.transcriptLang
                if (this.$transcriptLanguageSelect) {
                        this.$transcriptLanguageSelect.find('option:selected').prop('selected',false);
                        this.$transcriptLanguageSelect.find('option[lang=' + this.transcriptLang + ']').prop('selected',true);
                }
        }

        var thisObj = this;

        // Make transcript tabbable if preference is turned on.
        if (this.prefTabbable === 1) {
                $('.able-transcript span.able-transcript-seekpoint').attr('tabindex','0');
        }

        // handle clicks on text within transcript
        // Note: This event listeners handles clicks only, not keydown events
        // Pressing Enter on an element that is not natively clickable does NOT trigger click()
        // Keydown events are handled elsehwere, both globally (ableplayer-base.js) and locally (event.js)
        if (this.$transcriptArea.length > 0) {
                this.$transcriptArea.find('span.able-transcript-seekpoint').click(function(e) {
                        thisObj.seekTrigger = 'transcript';
                        var spanStart = parseFloat($(this).attr('data-start'));
                        // Add a tiny amount so that we're inside the span.
                        spanStart += .01;
                        // Each click within the transcript triggers two click events (not sure why)
                        // this.seekingFromTranscript is a stopgab to prevent two calls to SeekTo()
                        if (!thisObj.seekingFromTranscript) {
                                thisObj.seekingFromTranscript = true;
                                thisObj.seekTo(spanStart);
                        }
                        else {
                                // don't seek a second time, but do reset var
                                thisObj.seekingFromTranscript = false;
                        }
                });
        }
};

AblePlayer.prototype.highlightTranscript = function (currentTime) {

        //show highlight in transcript marking current caption

        if (!this.transcriptType) {
                return;
        }

        var start, end, isChapterHeading;
        var thisObj = this;

        currentTime = parseFloat(currentTime);

        // Highlight the current transcript item.
        this.$transcriptArea.find('span.able-transcript-seekpoint').each(function() {
                start = parseFloat($(this).attr('data-start'));
                end = parseFloat($(this).attr('data-end'));
                // be sure this isn't a chapter (don't highlight chapter headings)
                if ($(this).parent().hasClass('able-transcript-chapter-heading')) {
                        isChapterHeading = true;
                }
                else {
                        isChapterHeading = false;
                }

                if (currentTime >= start && currentTime <= end && !isChapterHeading) {
                        // move all previous highlights before adding one to current span
                        thisObj.$transcriptArea.find('.able-highlight').removeClass('able-highlight');
                        $(this).addClass('able-highlight');
                        return false;
                }
        });
        thisObj.currentHighlight = $('.able-highlight');
        if (thisObj.currentHighlight.length === 0) {
                // Nothing highlighted.
                thisObj.currentHighlight = null;
        }
};

AblePlayer.prototype.generateTranscript = function(chapters, captions, descriptions) {

        var thisObj = this;

        var $main = $('<div class="able-transcript-container"></div>');
        var transcriptTitle;

        // set language for transcript container
        $main.attr('lang', this.transcriptLang);

        if (typeof this.transcriptTitle !== 'undefined') {
                transcriptTitle = this.transcriptTitle;
        }
        else if (this.lyricsMode) {
                transcriptTitle = this.tt.lyricsTitle;
        }
        else {
                transcriptTitle = this.tt.transcriptTitle;
        }

        if (typeof this.transcriptDivLocation === 'undefined') {
                // only add an HTML heading to internal transcript
                // external transcript is expected to have its own heading
                var headingNumber = this.playerHeadingLevel;
                headingNumber += 1;
                var chapterHeadingNumber = headingNumber + 1;

                if (headingNumber <= 6) {
                        var transcriptHeading = 'h' + headingNumber.toString();
                }
                else {
                        var transcriptHeading = 'div';
                }
                // var transcriptHeadingTag = '<' + transcriptHeading + ' class="able-transcript-heading">';
                var $transcriptHeadingTag = $('<' + transcriptHeading + '>');
                $transcriptHeadingTag.addClass('able-transcript-heading');
                if (headingNumber > 6) {
                        $transcriptHeadingTag.attr({
                                'role': 'heading',
                                'aria-level': headingNumber
                        });
                }
                $transcriptHeadingTag.text(transcriptTitle);

                // set language of transcript heading to language of player
                // this is independent of language of transcript
                $transcriptHeadingTag.attr('lang', this.lang);

                $main.append($transcriptHeadingTag);
        }

        var nextChapter = 0;
        var nextCap = 0;
        var nextDesc = 0;

        var addChapter = function(div, chap) {

                if (chapterHeadingNumber <= 6) {
                        var chapterHeading = 'h' + chapterHeadingNumber.toString();
                }
                else {
                        var chapterHeading = 'div';
                }

                var $chapterHeadingTag = $('<' + chapterHeading + '>',{
                        'class': 'able-transcript-chapter-heading'
                });
                if (chapterHeadingNumber > 6) {
                        $chapterHeadingTag.attr({
                                'role': 'heading',
                                'aria-level': chapterHeadingNumber
                        });
                }

                var flattenComponentForChapter = function(comp) {

                        var result = [];
                        if (comp.type === 'string') {
                                result.push(comp.value);
                        }
                        else {
                                for (var i = 0; i < comp.children.length; i++) {
                                        result = result.concat(flattenComponentForChapter(comp.children[i]));
                                }
                        }
                        return result;
                }

                var $chapSpan = $('<span>',{
                        'class': 'able-transcript-seekpoint'
                });
                for (var i = 0; i < chap.components.children.length; i++) {
                        var results = flattenComponentForChapter(chap.components.children[i]);
                        for (var jj = 0; jj < results.length; jj++) {
                                $chapSpan.append(results[jj]);
                        }
                }
                $chapSpan.attr('data-start', chap.start.toString());
                $chapSpan.attr('data-end', chap.end.toString());
                $chapterHeadingTag.append($chapSpan);

                div.append($chapterHeadingTag);
        };

        var addDescription = function(div, desc) {
                var $descDiv = $('<div>', {
                        'class': 'able-transcript-desc'
                });
                var $descHiddenSpan = $('<span>',{
                        'class': 'able-hidden'
                });
                $descHiddenSpan.attr('lang', thisObj.lang);
                $descHiddenSpan.text(thisObj.tt.prefHeadingDescription + ': ');
                $descDiv.append($descHiddenSpan);

                var flattenComponentForDescription = function(comp) {

                        var result = [];
                        if (comp.type === 'string') {
                                result.push(comp.value);
                        }
                        else {
                                for (var i = 0; i < comp.children.length; i++) {
                                        result = result.concat(flattenComponentForDescription(comp.children[i]));
                                }
                        }
                        return result;
                }

                var $descSpan = $('<span>',{
                        'class': 'able-transcript-seekpoint'
                });
                for (var i = 0; i < desc.components.children.length; i++) {
                        var results = flattenComponentForDescription(desc.components.children[i]);
                        for (var jj = 0; jj < results.length; jj++) {
                                $descSpan.append(results[jj]);
                        }
                }
                $descSpan.attr('data-start', desc.start.toString());
                $descSpan.attr('data-end', desc.end.toString());
                $descDiv.append($descSpan);

                div.append($descDiv);
        };

        var addCaption = function(div, cap) {

                var $capSpan = $('<span>',{
                        'class': 'able-transcript-seekpoint able-transcript-caption'
                });

                var flattenComponentForCaption = function(comp) {

                        var result = [];

                        var parts = 0;

                        var flattenString = function (str) {

                                parts++;

                                var flatStr;
                                var result = [];
                                if (str === '') {
                                        return result;
                                }

                                var openBracket = str.indexOf('[');
                                var closeBracket = str.indexOf(']');
                                var openParen = str.indexOf('(');
                                var closeParen = str.indexOf(')');

                                var hasBrackets = openBracket !== -1 && closeBracket !== -1;
                                var hasParens = openParen !== -1 && closeParen !== -1;

                                if (hasParens || hasBrackets) {
                                        if (parts > 1) {
                                                // force a line break between sections that contain parens or brackets
                                                var silentSpanBreak = '<br/>';
                                        }
                                        else {
                                                var silentSpanBreak = '';
                                        }
                                        var silentSpanOpen = silentSpanBreak + '<span class="able-unspoken">';
                                        var silentSpanClose = '</span>';
                                        if (hasParens && hasBrackets) {
                                                // string has both!
                                                if (openBracket < openParen) {
                                                        // brackets come first. Parse parens separately
                                                        hasParens = false;
                                                }
                                                else {
                                                        // parens come first. Parse brackets separately
                                                        hasBrackets = false;
                                                }
                                        }
                                }
                                if (hasParens) {
                                        flatStr = str.substring(0, openParen);
                                        flatStr += silentSpanOpen;
                                        flatStr += str.substring(openParen, closeParen + 1);
                                        flatStr += silentSpanClose;
                                        flatStr += flattenString(str.substring(closeParen + 1));
                                        result.push(flatStr);
                                }
                                else if (hasBrackets) {
                                        flatStr = str.substring(0, openBracket);
                                        flatStr += silentSpanOpen;
                                        flatStr += str.substring(openBracket, closeBracket + 1);
                                        flatStr += silentSpanClose;
                                        flatStr += flattenString(str.substring(closeBracket + 1));
                                        result.push(flatStr);
                                }
                                else {
                                        result.push(str);
                                }
                                return result;
                        };

                        if (comp.type === 'string') {
                                result = result.concat(flattenString(comp.value));
                        }
                        else if (comp.type === 'v') {
                                var $vSpan = $('<span>',{
                                        'class': 'able-unspoken'
                                });
                                $vSpan.text('(' + comp.value + ')');
                                result.push($vSpan);
                                for (var i = 0; i < comp.children.length; i++) {
                                        var subResults = flattenComponentForCaption(comp.children[i]);
                                        for (var jj = 0; jj < subResults.length; jj++) {
                                                result.push(subResults[jj]);
                                        }
                                }
                        }
                        else if (comp.type === 'b' || comp.type === 'i') {
                                if (comp.type === 'b') {
                                        var $tag = $('<strong>');
                                }
                                else if (comp.type === 'i') {
                                        var $tag = $('<em>');
                                }
                                for (var i = 0; i < comp.children.length; i++) {
                                        var subResults = flattenComponentForCaption(comp.children[i]);
                                        for (var jj = 0; jj < subResults.length; jj++) {
                                                $tag.append(subResults[jj]);
                                        }
                                }
                                if (comp.type === 'b' || comp.type == 'i') {
                                        result.push($tag,' ');
                                }
                        }
                        else {
                                for (var i = 0; i < comp.children.length; i++) {
                                        result = result.concat(flattenComponentForCaption(comp.children[i]));
                                }
                        }
                        return result;
                };

                for (var i = 0; i < cap.components.children.length; i++) {
                        var results = flattenComponentForCaption(cap.components.children[i]);
                        for (var jj = 0; jj < results.length; jj++) {
                                var result = results[jj];
                                if (typeof result === 'string') {
                                        if (thisObj.lyricsMode) {
                                                // add <br> BETWEEN each caption and WITHIN each caption (if payload includes "\n")
                                                result = result.replace('\n','<br>') + '<br>';
                                        }
                                        else {
                                                // just add a space between captions
                                                result += ' ';
                                        }
                                }
                                $capSpan.append(result);
                        }
                }
                $capSpan.attr('data-start', cap.start.toString());
                $capSpan.attr('data-end', cap.end.toString());
                div.append($capSpan);
                div.append(' \n');
        };

        // keep looping as long as any one of the three arrays has content
        while ((nextChapter < chapters.length) || (nextDesc < descriptions.length) || (nextCap < captions.length)) {

                if ((nextChapter < chapters.length) && (nextDesc < descriptions.length) && (nextCap < captions.length)) {
                        // they all three have content
                        var firstStart = Math.min(chapters[nextChapter].start,descriptions[nextDesc].start,captions[nextCap].start);
                }
                else if ((nextChapter < chapters.length) && (nextDesc < descriptions.length)) {
                        // chapters & descriptions have content
                        var firstStart = Math.min(chapters[nextChapter].start,descriptions[nextDesc].start);
                }
                else if ((nextChapter < chapters.length) && (nextCap < captions.length)) {
                        // chapters & captions have content
                        var firstStart = Math.min(chapters[nextChapter].start,captions[nextCap].start);
                }
                else if ((nextDesc < descriptions.length) && (nextCap < captions.length)) {
                        // descriptions & captions have content
                        var firstStart = Math.min(descriptions[nextDesc].start,captions[nextCap].start);
                }
                else {
                        var firstStart = null;
                }
                if (firstStart !== null) {
                        if (typeof chapters[nextChapter] !== 'undefined' && chapters[nextChapter].start === firstStart) {
                                addChapter($main, chapters[nextChapter]);
                                nextChapter += 1;
                        }
                        else if (typeof descriptions[nextDesc] !== 'undefined' && descriptions[nextDesc].start === firstStart) {
                                addDescription($main, descriptions[nextDesc]);
                                nextDesc += 1;
                        }
                        else {
                                addCaption($main, captions[nextCap]);
                                nextCap += 1;
                        }
                }
                else {
                        if (nextChapter < chapters.length) {
                                addChapter($main, chapters[nextChapter]);
                                nextChapter += 1;
                        }
                        else if (nextDesc < descriptions.length) {
                                addDescription($main, descriptions[nextDesc]);
                                nextDesc += 1;
                        }
                        else if (nextCap < captions.length) {
                                addCaption($main, captions[nextCap]);
                                nextCap += 1;
                        }
                }
        }
        // organize transcript into blocks using [] and () as starting points
        var $components = $main.children();
        var spanCount = 0;
        var openBlock = true;
        $components.each(function() {
                if ($(this).hasClass('able-transcript-caption')) {
                        if ($(this).text().indexOf('[') !== -1 || $(this).text().indexOf('(') !== -1) {
                                // this caption includes a bracket or parenth. Start a new block
                                // close the previous block first
                                if (spanCount > 0) {
                                        $main.find('.able-block-temp').removeClass('able-block-temp').wrapAll('<div class="able-transcript-block"></div>');
                                        spanCount = 0;
                                }
                        }
                        $(this).addClass('able-block-temp');
                        spanCount++;
                }
                else {
                        // this is not a caption. Close the caption block
                        if (spanCount > 0) {
                                $main.find('.able-block-temp').removeClass('able-block-temp').wrapAll('<div class="able-transcript-block"></div>');
                                spanCount = 0;
                        }
                }
        });
        return $main;
};

})(jQuery);