/* Video Transcript Sorter (VTS)

* Used to synchronize time stamps from WebVTT resources
* so they appear in the proper sequence within an auto-generated interactive transcript

*/

(function ($) {

AblePlayer.prototype.injectVTS = function() {

        // To add a transcript sorter to a web page:
        // Add <div id="able-vts"></div> to the web page

        // Define all variables
        var thisObj, tracks, $heading;
        var $instructions, $p1, $p2, $ul, $li1, $li2, $li3;
        var $fieldset, $legend, i, $radioDiv, radioId, $label, $radio;
        var $saveButton, $savedTable;

        thisObj = this;

        if ($('#able-vts').length) {
                // Page includes a container for a VTS instance

                // Are they qualifying tracks?
                if (this.vtsTracks.length) {
                        // Yes - there are!

                        // Build an array of unique languages
                        this.langs = [];
                        this.getAllLangs(this.vtsTracks);

                        // Set the default VTS language
                        this.vtsLang = this.lang;

                        // Inject a heading
                        $heading = $('<h2>').text('Video Transcript Sorter'); // TODO: Localize; intelligently assign proper heading level
                        $('#able-vts').append($heading);

                        // Inject an empty div for writing messages
                        this.$vtsAlert = $('<div>',{
                                'id': 'able-vts-alert',
                                'aria-live': 'polite',
                                'aria-atomic': 'true'
                        })
                        $('#able-vts').append(this.$vtsAlert);

                        // Inject instructions (TODO: Localize)
                        $instructions = $('<div>',{
                                'id': 'able-vts-instructions'
                        });
                        $p1 = $('<p>').text('Use the Video Transcript Sorter to perform any of the following tasks:');
                        $ul = $('<ul>');
                        $li1 = $('<li>').text('Reorder chapters, descriptions, captions, and/or subtitles so they appear in the proper sequence in Able Player\'s auto-generated transcript.');
                        $li2 = $('<li>').text('Modify content or start/end times (all are directly editable within the table).');
                        $li3 = $('<li>').text('Insert new content, such as chapters or descriptions.');
                        $p2 = $('<p>').text('When finished editing, click the "Save Changes" button. This will auto-generate new content for all relevant timed text files (chapters, descriptions, captions, and/or subtitles), which can be copied and pasted into separate WebVTT files for use by Able Player.');
                        $ul.append($li1,$li2,$li3);
                        $instructions.append($p1,$ul,$p2);
                        $('#able-vts').append($instructions);

                        // Inject a fieldset with radio buttons for each language
                        $fieldset = $('<fieldset>');
                        $legend = $('<legend>').text('Select a language'); // TODO: Localize this
                        $fieldset.append($legend)
                        for (i in this.langs) {
                                radioId = 'vts-lang-radio-' + this.langs[i];
                                $radioDiv = $('<div>',{
                                        // uncomment the following if label is native name
                                        // 'lang': this.langs[i]
                                });
                                $radio = $('<input>', {
                                        'type': 'radio',
                                        'name': 'vts-lang',
                                        'id': radioId,
                                        'value': this.langs[i]
                                }).on('click',function() {
                                        thisObj.vtsLang = $(this).val();
                                        thisObj.showVtsAlert('Loading ' + thisObj.getLanguageName(thisObj.vtsLang) + ' tracks');
                                        thisObj.injectVtsTable('update',thisObj.vtsLang);
                                });
                                if (this.langs[i] == this.lang) {
                                        // this is the default language.
                                        $radio.prop('checked',true);
                                }
                                $label = $('<label>', {
                                        'for': radioId
                                        // Two options for label:
                                        // getLanguageNativeName() - returns native name; if using this be sure to add lang attr to <div> (see above)
                                        // getLanguageName() - returns name in English; doesn't require lang attr on <label>
                                }).text(this.getLanguageName(this.langs[i]));
                                $radioDiv.append($radio,$label);
                                $fieldset.append($radioDiv);
                        }
                        $('#able-vts').append($fieldset);

                        // Inject a 'Save Changes' button
                        $saveButton = $('<button>',{
                                'type': 'button',
                                'id': 'able-vts-save',
                                'value': 'save'
                        }).text('Save Changes'); // TODO: Localize this
                        $('#able-vts').append($saveButton);

                        // Inject a table with one row for each cue in the default language
                        this.injectVtsTable('add',this.vtsLang);

                        // TODO: Add drag/drop functionality for mousers

                        // Add event listeners for contenteditable cells
                        var kindOptions, beforeEditing, editedCell, editedContent, i, closestKind;
                        kindOptions = ['captions','chapters','descriptions','subtitles'];
                        $('td[contenteditable="true"]').on('focus',function() {
                                beforeEditing = $(this).text();
                        }).on('blur',function() {
                                if (beforeEditing != $(this).text()) {
                                        editedCell = $(this).index();
                                        editedContent = $(this).text();
                                        if (editedCell === 1) {
                                                // do some simple spelling auto-correct
                                                if ($.inArray(editedContent,kindOptions) === -1) {
                                                        // whatever user typed is not a valid kind
                                                        // assume they correctly typed the first character
                                                        if (editedContent.substr(0,1) === 's') {
                                                                $(this).text('subtitles');
                                                        }
                                                        else if (editedContent.substr(0,1) === 'd') {
                                                                $(this).text('descriptions');
                                                        }
                                                        else if (editedContent.substr(0,2) === 'ch') {
                                                                $(this).text('chapters');
                                                        }
                                                        else {
                                                                // whatever else they types, assume 'captions'
                                                                $(this).text('captions');
                                                        }
                                                }
                                        }
                                        else if (editedCell === 2 || editedCell === 3) {
                                                // start or end time
                                                // ensure proper formatting (with 3 decimal places)
                                                $(this).text(thisObj.formatTimestamp(editedContent));
                                        }
                                }
                        }).on('keydown',function(e) {
                                // don't allow keystrokes to trigger Able Player (or other) functions
                                // while user is editing
                                e.stopPropagation();
                        });

                        // handle click on the Save button

                        // handle click on the Save button
                        $('#able-vts-save').on('click',function(e) {
                                e.stopPropagation();
                                if ($(this).attr('value') == 'save') {
                                        // replace table with WebVTT output in textarea fields (for copying/pasting)
                                        $(this).attr('value','cancel').text('Return to Editor'); // TODO: Localize this
                                        $savedTable = $('#able-vts table');
                                        $('#able-vts-instructions').hide();
                                        $('#able-vts > fieldset').hide();
                                        $('#able-vts table').remove();
                                        $('#able-vts-icon-credit').remove();
                                        thisObj.parseVtsOutput($savedTable);
                                }
                                else {
                                        // cancel saving, and restore the table using edited content
                                        $(this).attr('value','save').text('Save Changes'); // TODO: Localize this
                                        $('#able-vts-output').remove();
                                        $('#able-vts-instructions').show();
                                        $('#able-vts > fieldset').show();
                                        $('#able-vts').append($savedTable);
                                        $('#able-vts').append(thisObj.getIconCredit());
                                        thisObj.showVtsAlert('Cancelling saving. Any edits you made have been restored in the VTS table.'); // TODO: Localize this
                                }
                        });
                }
        }
};

AblePlayer.prototype.setupVtsTracks = function(kind, lang, label, src, contents) {

        // Called from tracks.js

        var srcFile, vtsCues;

        srcFile = this.getFilenameFromPath(src);
        vtsCues = this.parseVtsTracks(contents);

        this.vtsTracks.push({
                'kind': kind,
                'language': lang,
                'label': label,
                'srcFile': srcFile,
                'cues': vtsCues
        });
};

AblePlayer.prototype.getFilenameFromPath = function(path) {

        var lastSlash;

        lastSlash = path.lastIndexOf('/');
        if (lastSlash === -1) {
                // there are no slashes in path.
                return path;
        }
        else {
                return path.substr(lastSlash+1);
        }
};

AblePlayer.prototype.getFilenameFromTracks = function(kind,lang) {

        for (var i=0; i<this.vtsTracks.length; i++) {
                if (this.vtsTracks[i].kind === kind && this.vtsTracks[i].language === lang) {
                        // this is a matching track
                        // srcFile has already been converted to filename from path before saving to vtsTracks
                        return this.vtsTracks[i].srcFile;
                }
        }
        // no matching track found
        return false;
};

AblePlayer.prototype.parseVtsTracks = function(contents) {

        var rows, timeParts, cues, i, j, thisRow, nextRow, content, blankRow;
        rows = contents.split("\n");
        cues = [];
        i = 0;
        while (i < rows.length) {
                thisRow = rows[i];
                if (thisRow.indexOf(' --> ') !== -1) {
                        // this is probably a time row
                        timeParts = thisRow.trim().split(' ');
                        if (this.isValidTimestamp(timeParts[0]) && this.isValidTimestamp(timeParts[2])) {
                                // both timestamps are valid. This is definitely a time row
                                content = '';
                                j = i+1;
                                blankRow = false;
                                while (j < rows.length && !blankRow) {
                                        nextRow = rows[j].trim();
                                        if (nextRow.length > 0) {
                                                if (content.length > 0) {
                                                        // add back the EOL between rows of content
                                                        content += "\n" + nextRow;
                                                }
                                                else {
                                                        // this is the first row of content. No need for an EOL
                                                        content += nextRow;
                                                }
                                        }
                                        else {
                                                blankRow = true;
                                        }
                                        j++;
                                }
                                cues.push({
                                        'start': timeParts[0],
                                        'end': timeParts[2],
                                        'content': content
                                });
                                i = j; //skip ahead
                        }
                }
                else {
                        i++;
                }
        }
        return cues;
};

AblePlayer.prototype.isValidTimestamp = function(timestamp) {

        // return true if timestamp contains only numbers or expected punctuation
        if (/^[0-9:,.]*$/.test(timestamp)) {
                return true;
        }
        else {
                return false;
        }
};

AblePlayer.prototype.formatTimestamp = function(timestamp) {

        // timestamp is a string in the form "HH:MM:SS.xxx"
        // Take some simple steps to ensure edited timestamp values still adhere to expected format

        var firstPart, lastPart;

        var firstPart = timestamp.substr(0,timestamp.lastIndexOf('.')+1);
        var lastPart = timestamp.substr(timestamp.lastIndexOf('.')+1);

        // TODO: Be sure each component within firstPart has only exactly two digits
        // Probably can't justify doing this automatically
        // If users enters '5' for minutes, that could be either '05' or '50'
        // This should trigger an error and prompt the user to correct the value before proceeding

        // Be sure lastPart has exactly three digits
        if (lastPart.length > 3) {
                // chop off any extra digits
                lastPart = lastPart.substr(0,3);
        }
        else if (lastPart.length < 3) {
                // add trailing zeros
                while (lastPart.length < 3) {
                        lastPart += '0';
                }
        }
        return firstPart + lastPart;
};

AblePlayer.prototype.injectVtsTable = function(action,lang) {

        // action is either 'add' (for a new table) or 'update' (if user has selected a new lang)

        var $table, headers, i, $tr, $th, $td, rows, rowNum, rowId;

        if (action === 'update') {
                // remove existing table
                $('#able-vts table').remove();
                $('#able-vts-icon-credit').remove();
        }

        $table = $('<table>',{
                'lang': lang
        });
        $tr = $('<tr>',{
                'lang': 'en' // TEMP, until header row is localized
        });
        headers = ['Row #','Kind','Start','End','Content','Actions']; // TODO: Localize this
        for (i=0; i < headers.length; i++) {
                $th = $('<th>', {
                        'scope': 'col'
                }).text(headers[i]);
                if (headers[i] === 'Actions') {
                        $th.addClass('actions');
                }
                $tr.append($th);
        }
        $table.append($tr);

        // Get all rows (sorted by start time), and inject them into table
        rows = this.getAllRows(lang);
        for (i=0; i < rows.length; i++) {
                rowNum = i + 1;
                rowId = 'able-vts-row-' + rowNum;
                $tr = $('<tr>',{
                        'id': rowId,
                        'class': 'kind-' + rows[i].kind
                });
                // Row #
                $td = $('<td>').text(rowNum);
                $tr.append($td);

                // Kind
                $td = $('<td>',{
                        'contenteditable': 'true'
                }).text(rows[i].kind);
                $tr.append($td);

                // Start
                $td = $('<td>',{
                        'contenteditable': 'true'
                }).text(rows[i].start);
                $tr.append($td);

                // End
                $td = $('<td>',{
                        'contenteditable': 'true'
                }).text(rows[i].end);
                $tr.append($td);

                // Content
                $td = $('<td>',{
                        'contenteditable': 'true'
                }).text(rows[i].content); // TODO: Preserve tags
                $tr.append($td);

                                // Actions
                $td = this.addVtsActionButtons(rowNum,rows.length);
                $tr.append($td);

                $table.append($tr);
        }
        $('#able-vts').append($table);

        // Add credit for action button SVG icons
        $('#able-vts').append(this.getIconCredit());

};

AblePlayer.prototype.addVtsActionButtons = function(rowNum,numRows) {

        // rowNum is the number of the current table row (starting with 1)
        // numRows is the total number of rows (excluding the header row)
        // TODO: Position buttons so they're vertically aligned, even if missing an Up or Down button
        var thisObj, $td, buttons, i, button, $button, $svg, $g, pathString, pathString2, $path, $path2;
        thisObj = this;
        $td = $('<td>');
        buttons = ['up','down','insert','delete'];

        for (i=0; i < buttons.length; i++) {
                button = buttons[i];
                if (button === 'up') {
                        if (rowNum > 1) {
                                $button = $('<button>',{
                                        'id': 'able-vts-button-up-' + rowNum,
                                        'title': 'Move up',
                                        'aria-label': 'Move Row ' + rowNum + ' up'
                                }).on('click', function(el) {
                                        thisObj.onClickVtsActionButton(el.currentTarget);
                                });
                                $svg = $('<svg>',{
                                        'focusable': 'false',
                                        'aria-hidden': 'true',
                                        'x': '0px',
                                        'y': '0px',
                                        'width': '254.296px',
                                        'height': '254.296px',
                                        'viewBox': '0 0 254.296 254.296',
                                        'style': 'enable-background:new 0 0 254.296 254.296'
                                });
                                pathString = 'M249.628,176.101L138.421,52.88c-6.198-6.929-16.241-6.929-22.407,0l-0.381,0.636L4.648,176.101'
                                        + 'c-6.198,6.897-6.198,18.052,0,24.981l0.191,0.159c2.892,3.305,6.865,5.371,11.346,5.371h221.937c4.577,0,8.613-2.161,11.41-5.594'
                                        + 'l0.064,0.064C255.857,194.153,255.857,182.998,249.628,176.101z';
                                $path = $('<path>',{
                                        'd': pathString
                                });
                                $g = $('<g>').append($path);
                                $svg.append($g);
                                $button.append($svg);
                                // Refresh button in the DOM in order for browser to process & display the SVG
                                $button.html($button.html());
                                $td.append($button);
                        }
                }
                else if (button === 'down') {
                        if (rowNum < numRows) {
                                $button = $('<button>',{
                                        'id': 'able-vts-button-down-' + rowNum,
                                        'title': 'Move down',
                                        'aria-label': 'Move Row ' + rowNum + ' down'
                                }).on('click', function(el) {
                                        thisObj.onClickVtsActionButton(el.currentTarget);
                                });
                                $svg = $('<svg>',{
                                        'focusable': 'false',
                                        'aria-hidden': 'true',
                                        'x': '0px',
                                        'y': '0px',
                                        'width': '292.362px',
                                        'height': '292.362px',
                                        'viewBox': '0 0 292.362 292.362',
                                        'style': 'enable-background:new 0 0 292.362 292.362'
                                });
                                pathString = 'M286.935,69.377c-3.614-3.617-7.898-5.424-12.848-5.424H18.274c-4.952,0-9.233,1.807-12.85,5.424'
                                        + 'C1.807,72.998,0,77.279,0,82.228c0,4.948,1.807,9.229,5.424,12.847l127.907,127.907c3.621,3.617,7.902,5.428,12.85,5.428'
                                        + 's9.233-1.811,12.847-5.428L286.935,95.074c3.613-3.617,5.427-7.898,5.427-12.847C292.362,77.279,290.548,72.998,286.935,69.377z';
                                $path = $('<path>',{
                                        'd': pathString
                                });
                                $g = $('<g>').append($path);
                                $svg.append($g);
                                $button.append($svg);
                                // Refresh button in the DOM in order for browser to process & display the SVG
                                $button.html($button.html());
                                $td.append($button);
                        }
                }
                else if (button === 'insert') {
                        // Add Insert button to all rows
                        $button = $('<button>',{
                                'id': 'able-vts-button-insert-' + rowNum,
                                'title': 'Insert row below',
                                'aria-label': 'Insert row before Row ' + rowNum
                        }).on('click', function(el) {
                                thisObj.onClickVtsActionButton(el.currentTarget);
                        });
                        $svg = $('<svg>',{
                                'focusable': 'false',
                                'aria-hidden': 'true',
                                'x': '0px',
                                'y': '0px',
                                'width': '401.994px',
                                'height': '401.994px',
                                'viewBox': '0 0 401.994 401.994',
                                'style': 'enable-background:new 0 0 401.994 401.994'
                        });
                        pathString = 'M394,154.175c-5.331-5.33-11.806-7.994-19.417-7.994H255.811V27.406c0-7.611-2.666-14.084-7.994-19.414'
                                + 'C242.488,2.666,236.02,0,228.398,0h-54.812c-7.612,0-14.084,2.663-19.414,7.993c-5.33,5.33-7.994,11.803-7.994,19.414v118.775'
                                + 'H27.407c-7.611,0-14.084,2.664-19.414,7.994S0,165.973,0,173.589v54.819c0,7.618,2.662,14.086,7.992,19.411'
                                + 'c5.33,5.332,11.803,7.994,19.414,7.994h118.771V374.59c0,7.611,2.664,14.089,7.994,19.417c5.33,5.325,11.802,7.987,19.414,7.987'
                                + 'h54.816c7.617,0,14.086-2.662,19.417-7.987c5.332-5.331,7.994-11.806,7.994-19.417V255.813h118.77'
                                + 'c7.618,0,14.089-2.662,19.417-7.994c5.329-5.325,7.994-11.793,7.994-19.411v-54.819C401.991,165.973,399.332,159.502,394,154.175z';
                        $path = $('<path>',{
                                'd': pathString
                        });
                        $g = $('<g>').append($path);
                        $svg.append($g);
                        $button.append($svg);
                        // Refresh button in the DOM in order for browser to process & display the SVG
                        $button.html($button.html());
                        $td.append($button);
                }
                else if (button === 'delete') {
                        // Add Delete button to all rows
                        $button = $('<button>',{
                                'id': 'able-vts-button-delete-' + rowNum,
                                'title': 'Delete row ',
                                'aria-label': 'Delete Row ' + rowNum
                        }).on('click', function(el) {
                                thisObj.onClickVtsActionButton(el.currentTarget);
                        });
                        $svg = $('<svg>',{
                                'focusable': 'false',
                                'aria-hidden': 'true',
                                'x': '0px',
                                'y': '0px',
                                'width': '508.52px',
                                'height': '508.52px',
                                'viewBox': '0 0 508.52 508.52',
                                'style': 'enable-background:new 0 0 508.52 508.52'
                        });
                        pathString = 'M397.281,31.782h-63.565C333.716,14.239,319.478,0,301.934,0h-95.347'
                                + 'c-17.544,0-31.782,14.239-31.782,31.782h-63.565c-17.544,0-31.782,14.239-31.782,31.782h349.607'
                                + 'C429.063,46.021,414.825,31.782,397.281,31.782z';
                        $path = $('<path>',{
                                'd': pathString
                        });
                        pathString2 = 'M79.456,476.737c0,17.544,14.239,31.782,31.782,31.782h286.042'
                                + 'c17.544,0,31.782-14.239,31.782-31.782V95.347H79.456V476.737z M333.716,174.804c0-8.772,7.151-15.891,15.891-15.891'
                                + 'c8.74,0,15.891,7.119,15.891,15.891v254.26c0,8.74-7.151,15.891-15.891,15.891c-8.74,0-15.891-7.151-15.891-15.891V174.804z'
                                + 'M238.369,174.804c0-8.772,7.119-15.891,15.891-15.891c8.74,0,15.891,7.119,15.891,15.891v254.26'
                                + 'c0,8.74-7.151,15.891-15.891,15.891c-8.772,0-15.891-7.151-15.891-15.891V174.804z M143.021,174.804'
                                + 'c0-8.772,7.119-15.891,15.891-15.891c8.772,0,15.891,7.119,15.891,15.891v254.26c0,8.74-7.119,15.891-15.891,15.891'
                                + 'c-8.772,0-15.891-7.151-15.891-15.891V174.804z';
                        $path2 = $('<path>',{
                                'd': pathString2
                        });

                        $g = $('<g>').append($path,$path2);
                        $svg.append($g);
                        $button.append($svg);
                        // Refresh button in the DOM in order for browser to process & display the SVG
                        $button.html($button.html());
                        $td.append($button);
                }
        }
        return $td;
};

AblePlayer.prototype.updateVtsActionButtons = function($buttons,nextRowNum) {

        // TODO: Add some filters to this function to add or delete 'Up' and 'Down' buttons
        // if row is moved to/from the first/last rows
        var i, $thisButton, id, label, newId, newLabel;
        for (i=0; i < $buttons.length; i++) {
                $thisButton = $buttons.eq(i);
                id = $thisButton.attr('id');
                label = $thisButton.attr('aria-label');
                // replace the integer (id) within each of the above strings
                newId = id.replace(/[0-9]+/g, nextRowNum);
                newLabel = label.replace(/[0-9]+/g, nextRowNum);
                $thisButton.attr('id',newId);
                $thisButton.attr('aria-label',newLabel);
        }
}

AblePlayer.prototype.getIconCredit = function() {

        var credit;
        credit = '<div id="able-vts-icon-credit">'
                + 'Action buttons made by <a href="https://www.flaticon.com/authors/elegant-themes">Elegant Themes</a> '
                + 'from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> '
                + 'are licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" '
                + 'target="_blank">CC 3.0 BY</a>'
                + '</div>';
                return credit;
};

AblePlayer.prototype.getAllLangs = function(tracks) {

        // update this.langs with any unique languages found in tracks
        var i;
        for (i in tracks) {
                if (tracks[i].hasOwnProperty('language')) {
                        if ($.inArray(tracks[i].language,this.langs) === -1) {
                                // this language is not already in the langs array. Add it.
                                this.langs[this.langs.length] = tracks[i].language;
                        }
                }
        }
};

AblePlayer.prototype.getAllRows = function(lang) {

        // returns an array of data to be displayed in VTS table
        // includes all cues for tracks of any type with matching lang
        // cues are sorted by start time
        var i, track, c, cues;
        cues = [];
        for (i=0; i < this.vtsTracks.length; i++) {
                track = this.vtsTracks[i];
                if (track.language == lang) {
                        // this track matches the language. Add its cues to array
                        for (c in track.cues) {
                                cues.push({
                                        'kind': track.kind,
                                        'lang': lang,
                                        'id': track.cues[c].id,
                                        'start': track.cues[c].start,
                                        'end': track.cues[c].end,
                                        'content': track.cues[c].content
                                });
                        }
                }
        }
        // Now sort cues by start time
        cues.sort(function(a,b) {
                return a.start > b.start ? 1 : -1;
        });
        return cues;
};

AblePlayer.prototype.onClickVtsActionButton = function(el) {

        // handle click on up, down, insert, or delete button
        var idParts, action, rowNum;
        idParts = $(el).attr('id').split('-');
        action = idParts[3];
        rowNum = idParts[4];
        if (action == 'up') {
                // move the row up
                this.moveRow(rowNum,'up');
        }
        else if (action == 'down') {
                // move the row down
                this.moveRow(rowNum,'down');
        }
        else if (action == 'insert') {
                // insert a row below
                this.insertRow(rowNum);
        }
        else if (action == 'delete') {
                // delete the row
                this.deleteRow(rowNum);
        }
};

AblePlayer.prototype.insertRow = function(rowNum) {

        // Insert empty row below rowNum
        var $table, $rows, numRows, newRowNum, newRowId, newTimes, $tr, $td;
        var $select, options, i, $option, newKind, newClass, $parentRow;
        var i, nextRowNum, $buttons;

        $table = $('#able-vts table');
        $rows = $table.find('tr');

        numRows = $rows.length - 1; // exclude header row

        newRowNum = parseInt(rowNum) + 1;
        newRowId = 'able-vts-row-' + newRowNum;

        // Create an empty row
        $tr = $('<tr>',{
                'id': newRowId
        });

        // Row #
        $td = $('<td>').text(newRowNum);
        $tr.append($td);

        // Kind (add a select field for chosing a kind)
        newKind = null;
        $select = $('<select>',{
                'id': 'able-vts-kind-' + newRowNum,
                'aria-label': 'What kind of track is this?',
                'placeholder': 'Select a kind'
        }).on('change',function() {
                newKind = $(this).val();
                newClass = 'kind-' + newKind;
                $parentRow = $(this).closest('tr');
                // replace the select field with the chosen value as text
                $(this).parent().text(newKind);
                // add a class to the parent row
                $parentRow.addClass(newClass);
        });
        options = ['','captions','chapters','descriptions','subtitles'];
        for (i=0; i<options.length; i++) {
                $option = $('<option>',{
                        'value': options[i]
                }).text(options[i]);
                $select.append($option);
        }
        $td = $('<td>').append($select);
        $tr.append($td);

        // Start
        $td = $('<td>',{
                'contenteditable': 'true'
        }); // TODO; Intelligently assign a new start time (see getAdjustedTimes())
        $tr.append($td);

        // End
        $td = $('<td>',{
                'contenteditable': 'true'
        }); // TODO; Intelligently assign a new end time (see getAdjustedTimes())
        $tr.append($td);

        // Content
        $td = $('<td>',{
                'contenteditable': 'true'
        });
        $tr.append($td);

        // Actions
        $td = this.addVtsActionButtons(newRowNum,numRows);
        $tr.append($td);

        // Now insert the new row
        $table.find('tr').eq(rowNum).after($tr);

        // Update row.id, Row # cell, & action items for all rows after the inserted one
        for (i=newRowNum; i <= numRows; i++) {
                nextRowNum = i + 1;
                $rows.eq(i).attr('id','able-vts-row-' + nextRowNum); // increment tr id
                $rows.eq(i).find('td').eq(0).text(nextRowNum); // increment Row # as expressed in first td
                $buttons = $rows.eq(i).find('button');
                this.updateVtsActionButtons($buttons,nextRowNum);
        }

        // Auto-adjust times
        this.adjustTimes(newRowNum);

        // Announce the insertion
        this.showVtsAlert('A new row ' + newRowNum + ' has been inserted'); // TODO: Localize this

        // Place focus in new select field
        $select.focus();

};

AblePlayer.prototype.deleteRow = function(rowNum) {

        var $table, $rows, numRows, i, nextRowNum, $buttons;

        $table = $('#able-vts table');
        $table[0].deleteRow(rowNum);
        $rows = $table.find('tr'); // this does not include the deleted row
        numRows = $rows.length - 1; // exclude header row

        // Update row.id, Row # cell, & action buttons for all rows after the deleted one
        for (i=rowNum; i <= numRows; i++) {
                nextRowNum = i;
                $rows.eq(i).attr('id','able-vts-row-' + nextRowNum); // increment tr id
                $rows.eq(i).find('td').eq(0).text(nextRowNum); // increment Row # as expressed in first td
                $buttons = $rows.eq(i).find('button');
                this.updateVtsActionButtons($buttons,nextRowNum);
        }

        // Announce the deletion
        this.showVtsAlert('Row ' + rowNum + ' has been deleted'); // TODO: Localize this

};

AblePlayer.prototype.moveRow = function(rowNum,direction) {

        // swap two rows
        var $rows, $thisRow, otherRowNum, $otherRow, newTimes, msg;

        $rows = $('#able-vts table').find('tr');
        $thisRow = $('#able-vts table').find('tr').eq(rowNum);
        if (direction == 'up') {
                otherRowNum = parseInt(rowNum) - 1;
                $otherRow = $('#able-vts table').find('tr').eq(otherRowNum);
                $otherRow.before($thisRow);
        }
        else if (direction == 'down') {
                otherRowNum = parseInt(rowNum) + 1;
                $otherRow = $('#able-vts table').find('tr').eq(otherRowNum);
                $otherRow.after($thisRow);
        }
        // Update row.id, Row # cell, & action buttons for the two swapped rows
        $thisRow.attr('id','able-vts-row-' + otherRowNum);
        $thisRow.find('td').eq(0).text(otherRowNum);
        this.updateVtsActionButtons($thisRow.find('button'),otherRowNum);
        $otherRow.attr('id','able-vts-row-' + rowNum);
        $otherRow.find('td').eq(0).text(rowNum);
        this.updateVtsActionButtons($otherRow.find('button'),rowNum);

        // auto-adjust times
        this.adjustTimes(otherRowNum);

        // Announce the move (TODO: Localize this)
        msg = 'Row ' + rowNum + ' has been moved ' + direction;
        msg += ' and is now Row ' + otherRowNum;
        this.showVtsAlert(msg);
};

AblePlayer.prototype.adjustTimes = function(rowNum) {

        // Adjusts start and end times of the current, previous, and next rows in VTS table
        // after a move or insert
        // NOTE: Fully automating this process would be extraordinarily complicated
        // The goal here is simply to make subtle tweaks to ensure rows appear
        // in the new order within the Able Player transcript
        // Additional tweaking will likely be required by the user

        // HISTORY: Originally set minDuration to 2 seconds for captions and .500 for descriptions
        // However, this can results in significant changes to existing caption timing,
        // with not-so-positive results.
        // As of 3.1.15, setting minDuration to .001 for all track kinds
        // Users will have to make further adjustments manually if needed

        // TODO: Add WebVTT validation on save, since tweaking times is risky

        var      minDuration, $rows, prevRowNum, nextRowNum, $row, $prevRow, $nextRow,
                        kind, prevKind, nextKind,
                        start, prevStart, nextStart,
                        end, prevEnd, nextEnd;

        // Define minimum duration (in seconds) for each kind of track
        minDuration = [];
        minDuration['captions'] = .001;
        minDuration['descriptions'] = .001;
        minDuration['chapters'] = .001;

        // refresh rows object
        $rows = $('#able-vts table').find('tr');

        // Get kind, start, and end from current row
        $row = $rows.eq(rowNum);
        if ($row.is('[class^="kind-"]')) {
                // row has a class that starts with "kind-"
                // Extract kind from the class name
                kind = this.getKindFromClass($row.attr('class'));
        }
        else {
                // Kind has not been assigned (e.g., newly inserted row)
                // Set as captions row by default
                kind = 'captions';
        }
        start = this.getSecondsFromColonTime($row.find('td').eq(2).text());
        end = this.getSecondsFromColonTime($row.find('td').eq(3).text());

        // Get kind, start, and end from previous row
        if (rowNum > 1) {
                // this is not the first row. Include the previous row
                prevRowNum = rowNum - 1;
                $prevRow = $rows.eq(prevRowNum);
                if ($prevRow.is('[class^="kind-"]')) {
                        // row has a class that starts with "kind-"
                        // Extract kind from the class name
                 prevKind = this.getKindFromClass($prevRow.attr('class'));
                }
                else {
                        // Kind has not been assigned (e.g., newly inserted row)
                        prevKind = null;
                }
                prevStart = this.getSecondsFromColonTime($prevRow.find('td').eq(2).text());
                prevEnd = this.getSecondsFromColonTime($prevRow.find('td').eq(3).text());
        }
        else {
                // this is the first row
                prevRowNum = null;
                $prevRow = null;
                prevKind = null;
                prevStart = null;
                prevEnd = null;
        }

        // Get kind, start, and end from next row
        if (rowNum < ($rows.length - 1)) {
                // this is not the last row. Include the next row
                nextRowNum = rowNum + 1;
                $nextRow = $rows.eq(nextRowNum);
                if ($nextRow.is('[class^="kind-"]')) {
                        // row has a class that starts with "kind-"
                        // Extract kind from the class name
                 nextKind = this.getKindFromClass($nextRow.attr('class'));
                }
                else {
                        // Kind has not been assigned (e.g., newly inserted row)
                        nextKind = null;
                }
                nextStart = this.getSecondsFromColonTime($nextRow.find('td').eq(2).text());
                nextEnd = this.getSecondsFromColonTime($nextRow.find('td').eq(3).text());
        }
        else {
                // this is the last row
                nextRowNum = null;
                $nextRow = null;
                nextKind = null;
                nextStart = null;
                nextEnd = null;
        }

        if (isNaN(start)) {
                if (prevKind == null) {
                        // The previous row was probably inserted, and user has not yet selected a kind
                        // automatically set it to captions
                        prevKind = 'captions';
                        $prevRow.attr('class','kind-captions');
                        $prevRow.find('td').eq(1).html('captions');
                }
                // Current row has no start time (i.e., it's an inserted row)
                if (prevKind === 'captions') {
                        // start the new row immediately after the captions end
                        start = (parseFloat(prevEnd) + .001).toFixed(3);
                        if (nextStart) {
                                // end the new row immediately before the next row starts
                                end = (parseFloat(nextStart) - .001).toFixed(3);
                        }
                        else {
                                // this is the last row. Use minDuration to calculate end time.
                                end = (parseFloat(start) + minDuration[kind]).toFixed(3);
                        }
                }
                else if (prevKind === 'chapters') {
                        // start the new row immediately after the chapter start (not end)
                        start = (parseFloat(prevStart) + .001).toFixed(3);
                        if (nextStart) {
                                // end the new row immediately before the next row starts
                                end = (parseFloat(nextStart) - .001).toFixed(3);
                        }
                        else {
                                // this is the last row. Use minDuration to calculate end time.
                                end = (parseFloat(start) + minDurartion[kind]).toFixed(3);
                        }
                }
                else if (prevKind === 'descriptions') {
                        // start the new row minDuration['descriptions'] after the description starts
                        // this will theoretically allow at least a small cushion for the description to be read
                        start = (parseFloat(prevStart) + minDuration['descriptions']).toFixed(3);
                        end = (parseFloat(start) + minDuration['descriptions']).toFixed(3);
                }
        }
        else {
                // current row has a start time (i.e., an existing row has been moved))
                if (prevStart) {
                        // this is not the first row.
                        if (prevStart < start) {
                                if (start < nextStart) {
                                        // No change is necessary
                                }
                                else {
                                        // nextStart needs to be incremented
                                        nextStart = (parseFloat(start) + minDuration[kind]).toFixed(3);
                                        nextEnd = (parseFloat(nextStart) + minDuration[nextKind]).toFixed(3);
                                        // TODO: Ensure nextEnd does not exceed the following start (nextNextStart)
                                        // Or... maybe this is getting too complicated and should be left up to the user
                                }
                        }
                        else {
                                // start needs to be incremented
                                start = (parseFloat(prevStart) + minDuration[prevKind]).toFixed(3);
                                end = (parseFloat(start) + minDuration[kind]).toFixed(3);
                        }
                }
                else {
                        // this is the first row
                        if (start < nextStart) {
                                // No change is necessary
                        }
                        else {
                                // nextStart needs to be incremented
                                nextStart = (parseFloat(start) + minDuration[kind]).toFixed(3);
                                nextEnd = (parseFloat(nextStart) + minDuration[nextKind]).toFixed(3);
                        }
                }
        }

        // check to be sure there is sufficient duration between new start & end times
        if (end - start < minDuration[kind]) {
                // duration is too short. Change end time
                end = (parseFloat(start) + minDuration[kind]).toFixed(3);
                if (nextStart) {
                        // this is not the last row
                        // increase start time of next row
                        nextStart = (parseFloat(end) + .001).toFixed(3);
                }
        }

        // Update all affected start/end times
        $row.find('td').eq(2).text(this.formatSecondsAsColonTime(start,true));
        $row.find('td').eq(3).text(this.formatSecondsAsColonTime(end,true));
        if ($prevRow) {
                $prevRow.find('td').eq(2).text(this.formatSecondsAsColonTime(prevStart,true));
                $prevRow.find('td').eq(3).text(this.formatSecondsAsColonTime(prevEnd,true));
        }
        if ($nextRow) {
                $nextRow.find('td').eq(2).text(this.formatSecondsAsColonTime(nextStart,true));
                $nextRow.find('td').eq(3).text(this.formatSecondsAsColonTime(nextEnd,true));
        }
};

AblePlayer.prototype.getKindFromClass = function(myclass) {

        // This function is called when a class with prefix "kind-" is found in the class attribute
        // TODO: Rewrite this using regular expressions
        var kindStart, kindEnd, kindLength, kind;

        kindStart = myclass.indexOf('kind-')+5;
        kindEnd = myclass.indexOf(' ',kindStart);
        if (kindEnd == -1) {
                // no spaces found, "kind-" must be the only myclass
                kindLength = myclass.length - kindStart;
        }
        else {
                kindLength = kindEnd - kindStart;
        }
        kind = myclass.substr(kindStart,kindLength);
        return kind;
};

AblePlayer.prototype.showVtsAlert = function(message) {

        // this is distinct from greater Able Player showAlert()
        // because it's positioning needs are unique
        // For now, alertDiv is fixed at top left of screen
        // but could ultimately be modified to appear near the point of action in the VTS table
        this.$vtsAlert.text(message).show().delay(3000).fadeOut('slow');
};

AblePlayer.prototype.parseVtsOutput = function($table) {

        // parse table into arrays, then into WebVTT content, for each kind
        // Display the WebVTT content in textarea fields for users to copy and paste
        var lang, i, kinds, kind, vtt, $rows, start, end, content, $output;

        lang = $table.attr('lang');
        kinds = ['captions','chapters','descriptions','subtitles'];
        vtt = {};
        for (i=0; i < kinds.length; i++) {
                kind = kinds[i];
                vtt[kind] = 'WEBVTT' + "\n\n";
        }
        $rows = $table.find('tr');
        if ($rows.length > 0) {
                for (i=0; i < $rows.length; i++) {
                        kind = $rows.eq(i).find('td').eq(1).text();
                        if ($.inArray(kind,kinds) !== -1) {
                                start = $rows.eq(i).find('td').eq(2).text();
                                end = $rows.eq(i).find('td').eq(3).text();
                                content = $rows.eq(i).find('td').eq(4).text();
                                if (start !== undefined && end !== undefined) {
                                        vtt[kind] += start + ' --> ' + end + "\n";
                                        if (content !== 'undefined') {
                                                vtt[kind] += content;
                                        }
                                        vtt[kind] += "\n\n";
                                }
                        }
                }
        }
        $output = $('<div>',{
                'id': 'able-vts-output'
        })
        $('#able-vts').append($output);
        for (i=0; i < kinds.length; i++) {
                kind = kinds[i];
                if (vtt[kind].length > 8) {
                        // some content has been added
                        this.showWebVttOutput(kind,vtt[kind],lang)
                }
        }
};

AblePlayer.prototype.showWebVttOutput = function(kind,vttString,lang) {

        var $heading, filename, $p, pText, $textarea;

        $heading = $('<h3>').text(kind.charAt(0).toUpperCase() + kind.slice(1));
        filename = this.getFilenameFromTracks(kind,lang);
        pText = 'If you made changes, copy/paste the following content ';
        if (filename) {
                pText += 'to replace the original content of your ' + this.getLanguageName(lang) + ' ';
                pText += '<em>' + kind + '</em> WebVTT file (<strong>' + filename + '</strong>).';
        }
        else {
                pText += 'into a new ' + this.getLanguageName(lang) + ' <em>' + kind + '</em> WebVTT file.';
        }
        $p = $('<p>',{
                'class': 'able-vts-output-instructions'
        }).html(pText);
        $textarea = $('<textarea>').text(vttString);
        $('#able-vts-output').append($heading,$p,$textarea);
};

})(jQuery);