//  HULU CONFIDENTIAL MATERIAL. DO NOT DISTRIBUTE.
//  Copyright (C) 2009-2010 Hulu, LLC
//  All Rights Reserved
/**
 * htvUtils.js 
 * 
 * Contains Utility methods and classes.
 */
var $htv;
/*jslint nomen: false, debug: true, evil: false, immed: false, plusplus: false*/

$htv.Utils = function () {

    var _parsedRequestCache = {};

    function _applyGlobalContext(url, package_id) {
        var pkg = 2;
        if (package_id !== undefined && package_id !== null && package_id > 0) {
            pkg = package_id;
        }
        
        if (url.indexOf('?') !== -1) {
            url = url + "&dp_id=hulu&device_id=2&package_id=" + pkg;
        } else {
            url = url + "?dp_id=hulu&device_id=2&package_id=" + pkg;
        }
        return url;
    }
    
    function _copyObject(source) {
        var dest = {};
        for (var key in source) {
            dest[key] = source[key];
        }
        return dest;
    }
    
    // Convenience to retrieve a new zero-valued coordinate
    function _createPosition(x,y,z,width,height) {
        return {
            x: x || 0,
            y: y || 0,
            z: z || 0,
            width: width || 0,
            height: height || 0
        };
    }
    
    function _getEndCreditsTimeFromString(end_credits_time) {
        if (_stringIsNullOrEmpty(end_credits_time)) {
            return 0;
        }
        try {
            var ectArray = end_credits_time.split(';')[0].split(':');
            return (parseInt(ectArray[0] * 3600, 10) + parseInt(ectArray[1] * 60, 10) + parseInt(ectArray[2], 10)) * 1000;    
        }
        catch(e) {
            return 0;
        }
    }

    function _getKinkoQueryString(options) {
        var qs = "";
        if (options !== undefined) {
            if (options.width !== undefined && options.height !== undefined) {
                if ($htv.Platform.properties.vSize == 540) {
                    qs += "&size=" + options.width +
                    "x" +
                    options.height;
                } else {
                    qs += "&size=" + options.width * 2 +
                    "x" +
                    options.height * 2;
                }
            }
            if (options.format !== undefined) {
                qs += "&format=" + options.format;
            }
            if (options.reflection_alpha_top !== undefined) {
                qs += "&reflection_alpha_top=" + options.reflection_alpha_top;
            }
            if (options.reflection_height !== undefined) {
                qs += "&reflection_height=" + options.reflection_height;
            }
            if (options.bug_width !== undefined && options.bug_height !== undefined) {
                qs += "&bug_size=" + options.bug_width
                    + "x" + options.bug_height;
            }
            if (options.maintain_ratio !== undefined) {
                qs += "&maintain_ratio=" + (options.maintain_ratio === true ? 1 : 0);
            }
            // qs += "&postprocess_filter=sharpen";
        }
        return qs.substr(1);
    }
    
    function _getURLForAsset(url, options) {
        url = url.replace("http://assets.hulu.com", $htv.Constants.Endpoints.kinko + "/assets");
        url = url.replace("http://assets.huluim.com", $htv.Constants.Endpoints.kinko + "/assets");
        var qs = _getKinkoQueryString(options);
        if (qs.length > 0) {
            url += (url.indexOf("?") == -1 ? "?" : "&") + qs;
        }
        return url;
    }
    
    function _getURLForChannelThumb(channel_canonical_name, options) {
        var url = $htv.Constants.Endpoints.kinko + "/channel/" + channel_canonical_name;
        var qs = _getKinkoQueryString(options);
        if (qs.length > 0) {
            url += "?" + qs;
        }
        return url;
    }
    
    function _getURLForCompanyLogo(company_id, options) {
        var url = $htv.Constants.Endpoints.kinko + "/company_logo/" + company_id;
        var qs = _getKinkoQueryString(options);
        if (qs.length > 0) {
            url += "?" + qs;
        }
        return url;
    }
    
    function _getURLForCompanyThumb(company_id, options) {
        var url = $htv.Constants.Endpoints.kinko + "/company/" + company_id;
        var qs = _getKinkoQueryString(options);
        if (qs.length > 0) {
            url += "?" + qs;
        }
        return url;
    }
    
    function _getURLForShowLogo(show_id, options) {
        var url = $htv.Constants.Endpoints.kinko + "/show_logo/" + show_id;
        var qs = _getKinkoQueryString(options);
        if (qs.length > 0) {
            url += "?" + qs;
        }
        return url;
    }
    
    function _getURLForShowThumb(show_id, options) {
        var url = $htv.Constants.Endpoints.kinko + "/show/" + show_id;
        var qs = _getKinkoQueryString(options);
        if (qs.length > 0) {
            url += "?" + qs;
        }
        return url;
    }
    
    function _getURLForVideoThumb(content_id, options) {
        var url = $htv.Constants.Endpoints.kinko + "/video/" + content_id;
        var qs = _getKinkoQueryString(options);
        if (qs.length > 0) {
            url += "?" + qs;
        }
        return url;
    }
    
    // Based on a twinkie result, generate a metadata text blob.
    function _formatVideoSubtitle(item) {
        if (null !== item) {
            var programmingType = item.programming_type;
            if (programmingType == "Full Episode") {
                if (item.season_number !== null && item.episode_number !== null) {
                    programmingType = "Season " + item.season_number + " : Episode " + item.episode_number;
                }
                else if (item.season_number === null && item.episode_number !== null) {
                    programmingType = "Episode " + item.episode_number;
                }
                else if (item.season_number !== null && item.episode_number === null) {
                    programmingType = "Season " + item.season_number;
                }
                else { //if (item.season_number === null && item.episode_number === null) {
                    // Do nothing (leave as "Full Episode")
                }
            }
            return programmingType + " (" + $htv.Utils.formatSecondsAsTimeCode(item.duration) + ")";
        }
        return "";
    }
    
    // Includes hours if > 60m
    function _formatSecondsAsTimeCode(seconds, padZeros) {
        // Default to padded zerios
        if (padZeros === undefined) {
            padZeros = true;
        }
        
        var secondsInt = parseInt(seconds, 10);
        var h = Math.floor(secondsInt / 3600);
        var m = Math.floor((secondsInt % 3600) / 60);
        var s = Math.floor((secondsInt % 3600) % 60);
        
        var ret = (h === 0 ? "" : (h < 10 && padZeros ? "0" + String(h) : String(h)) + ":");
        ret += ((m < 10 && (ret.length > 0 || padZeros)) ? "0" + String(m) : String(m)) + ":";
        ret += (s < 10 ? "0" + String(s) : String(s));
        return ret;
    }
    
    function _typeOf(o) {
        if (typeof(o) == "object") {
            if (o === null) return "null";
            if (o.constructor == (new Array).constructor) return "array";
            if (o.constructor == (new Date).constructor) return "date";
            if (o.constructor == (new RegExp).constructor) return "regex";
            return "object";
        }
        return typeof(o);
    }
                
    var _twinkieJSONTable = {
        display: "title",
        items_url: "items_url",
        data: {
            thumbnail_url_16x9_large: "thumb_url",
            programming_type: "programming_type",
            season_number: "season_number",
            episode_number: "episode_number",
            description: "description",
            content_id: "content_id",
            video_id: "video_id",
            show_id: "show_id",
            show_name: "show_name",
            show_canonical_name: "show_canonical_name",
            rating: "rating",
            canonical_name: "canonical_name",
            pid: "pid",
            genre: "genre",
            duration: "duration",
            media_type: "media_type",
            has_captions: "has_captions",
            company_name: "company_name",
            company_id: "company_id",
            feature_film_id: "feature_film_id",
            feature_film_content_id: "feature_film_content_id",
            name: "name",
            videos_count: "videos_count",
            full_episodes_count: "full_episodes_count",
            clips_count: "clips_count",
            feature_film_count: "feature_film_count",
            collation_name: "collation_name",
            collation_title: "collation_title",
            plus_living_room_expires_at: "plus_living_room_expires_at",
            include_company_logo: "include_company_logo"
        },
        app_data: {
            view_name: "view_name",
            // todo: remove surprise renames like this
            full_name: "carousel_name",
            item_type: "carousel_item_type",
            cmtype: "cmtype",
            use_related_title: "use_related_title",
            include_show_title: "include_show_title",
            start_at_end: "start_at_end",
            login_required: "login_required",
            add_cb: "add_cb",
            recommended_required: "recommended_required",
            bug_br_field: "bug_br_field",
            child_text: "child_text",
            is_queue_carousel : "is_queue_carousel"
        }
    };

    function GenericCollection(source_array) {
        this.items = source_array;

        this.getLength = function () {
            return this.items.length;
        };

        this.getMetadataItem = function () {
            return null;
        };
        
        this.getItemAt = function (index) {
            return {
                success: true,
                item: this.items[index]
            };
        };
        
        this.setItemAt = function(index,item) {
            this.items[index] = item;
        };
        
        this.initialize = function (url, receiver, callback, asyncClosure) {
            // immediately done
            if (callback && receiver) {
                callback.call(receiver);
            }
        };
    }
    
    // Utility class defining a paged collection.
    function PagedTwinkieCollection(required_fields, page_size) {
        // allows the consumer to designate a callback for when items can be requested
        this.resultsCallback = null;
        this.activePageNumber = 1;
        this.fetchedPages = [];
        this.totalCount = -1;
        this.excludedCount = 0;
        this.initializationState = 0;
        this.menuUrl = null;
        this.numPages = 0;
        this.asyncClosure = null;
        this.jumpLoading = -1;
        this.metadataItem = null;
        this.activePageFetches = null;
        this.fields = {};
        
        this.pageSize = $htv.Constants.DEFAULT_PAGE_SIZE;
        if (page_size !== undefined) {
            this.pageSize = page_size;
        }
        
        // Need an object to make lookups fast
        if (required_fields !== undefined) {
            for (var i = 0; i < required_fields.length; i++) {
                this.fields[required_fields[i]] = true;
            }
        } else {
            this.fields = {
                items_url: true,
                title: true
            };
        }
        
        this.getLength = function () {
            return Math.max(0, this.totalCount - this.excludedCount);
        };
        
        this.getMetadataItem = function () {
            return this.metadataItem;
        };
        
        this.getItemAt = function (index) {
            var indexPage = this.pageSize === 0 ? 1 : Math.floor(index / this.pageSize) + 1;
            if (this.fetchedPages[indexPage] && this.fetchedPages[indexPage].length >= (index % this.pageSize)) {
                if ((index % this.pageSize) > 0.8 * this.pageSize) {
                    this.fetchPage(indexPage + 1);
                } 
                if ((index % this.pageSize) < 0.2 * this.pageSize) {
                    this.fetchPage(indexPage - 1);
                } 
                return {success: true, item: this.fetchedPages[indexPage][(index % this.pageSize)]};
            } else {
                this.jumpToPage(indexPage);
                return {success: false, item: null};
            }
        };
        
        this.jumpToPage = function(pageNumber) {
            if (this.jumpLoading == -1 || this.jumpLoading != pageNumber) {
                this.jumpLoading = pageNumber;
                
                
                this.loadURLWithCache(this.menuUrl + "&page=" + pageNumber, this, this.jumpResult, {
                    xmlparse: false,
                    pagenum: pageNumber
                });
                this.fetchPage(pageNumber + 1);
                this.fetchPage(pageNumber - 1);
            } else {
                
            }
        };
        
        this.loadURLWithCache = function(url, receiver, callback, options) {
            if (_parsedRequestCache[url]) {
                
                callback.call(this, _parsedRequestCache[url], options);
            }
            else {
                $htv.Platform.loadURL(url, this, function(responseData, responseOptions) {
                    var responseJSON = $htv.Platform.parseJSON(responseData);
                    if (responseOptions.cache_response === true) {
                        
                        _parsedRequestCache[url] = responseJSON;
                    }
                    callback.call(this, responseJSON, responseOptions);
                }, options);
            }
        };
        
        this.jumpResult = function(responseJSON, options) {
            
            this.fetchedPages[options.pagenum] = [];
            this.jsonParse(responseJSON.item, this.fetchedPages[options.pagenum], responseJSON.data);
            this.jumpLoading = -1;
            
            if (this.asyncClosure) {
                this.asyncClosure.callback.call(this.asyncClosure.receiver);
            }
        };
        
        this.jsonParse = function(sourceObject, destinationArray, sourceExclusionData) {
            if (!sourceObject) {
                return;
            }
            else if (_typeOf(sourceObject) == "object") {
                // Bug (feature?) in HPC JSON encoder: single element arrays are returned as objects
                sourceObject = [ sourceObject ];
            }
            
            var count;
            var newIndex = 0;
            for (var i = 0; i < sourceObject.length; i++) {
                if (sourceObject[i].app_data && sourceExclusionData) {
                    // If field specified, exclude based on count from metadata item
                    if (sourceObject[i].app_data.hasOwnProperty("required_count_field")) {
                        count = parseInt(sourceExclusionData[sourceObject[i].app_data.required_count_field], 10);
                        if (isNaN(count) || count <= 0) {
                            this.excludedCount++;
                            continue;
                        }
                    }                    
                    // If field specified, append count from metadata item
                    if (sourceObject[i].app_data.hasOwnProperty("append_count_field")) {
                        count = parseInt(sourceExclusionData[sourceObject[i].app_data.append_count_field], 10);
                        if (!isNaN(count)) {
                            sourceObject[i].display += " (" + count + ")";
                        }
                    }
                }
                destinationArray[newIndex] = {};
                this.jsonParseHelper(sourceObject[i], destinationArray[newIndex], _twinkieJSONTable);
                newIndex++;
            }
        };
        
        this.jsonParseHelper = function(source, destination, table) {
            for (var sourceKey in table) {
                if (typeof(table[sourceKey]) == "string") {
                    // Check if table[sourceKey] was specified in the required_fields constructor arg
                    if (this.fields[table[sourceKey]]) {
                        destination[table[sourceKey]] = source[sourceKey];
                    }
                }
                else if (source[sourceKey]) {
                    this.jsonParseHelper(source[sourceKey], destination, table[sourceKey]);
                }
            }
        };
        
        this.initialize = function (url, receiver, callback, asyncClosure, options) {
            this.resultsCallback = callback;
            this.resultsReceiver = receiver;
            this.asyncClosure = asyncClosure;
            this.activePageFetches = {};
            
            this.menuUrl = url
                + "&limit=" + this.pageSize
                + "&format_json=1";

            this.loadURLWithCache(this.menuUrl, this, this.onInitializeResult, {
                xmlparse: false,
                cache_response: options && options.cache_response
            });
            this.loadURLWithCache(this.menuUrl + "&total=1", this, this.onTotalResult, {
                xmlparse: false,
                cache_response: options && options.cache_response
            });
        };
        
        // total and data received from twinkie
        this.initializeComplete = function () {
            if (this.totalCount === 0) {
                this.numPages = 0;
            } else {
                this.numPages = Math.ceil(this.totalCount / this.pageSize);
            }
            this.fetchPage(2);
            this.resultsCallback.call(this.resultsReceiver);            
        };
        
        this.onInitializeResult = function (responseJSON, options) {
            this.fetchedPages[1] = [];
            this.metadataItem = responseJSON.data;
            this.jsonParse(responseJSON.item, this.fetchedPages[1], responseJSON.data);
            this.initializationState++;
            if (this.initializationState === 2) {
                this.initializeComplete();
            }
        };
        
        this.onFetchResult = function (responseJSON, options) {
            
            this.activePageFetches[options.pagenum] = false;
            delete this.activePageFetches[options.pagenum];
            if (this.fetchedPages[options.pagenum]) {
                
            }
            this.fetchedPages[options.pagenum] = [];
            this.jsonParse(responseJSON.item, this.fetchedPages[options.pagenum], responseJSON.data);
        };
        
        this.onTotalResult = function (responseJSON, options) {
            var tc = responseJSON.total_count;
            if (tc === null) {
                this.totalCount = 0;
            }
            else {
                this.totalCount = parseInt(tc, 10);
            }
            this.initializationState++;
            if (this.initializationState === 2) {
                this.initializeComplete();
            }
        };
        
        this.fetchPage = function (pageNum) {
            if (pageNum >= 1 && pageNum <= this.numPages) {
                if (this.activePageFetches[pageNum] === true) {
                    // 
                    return;
                }
                if (this.fetchedPages[pageNum]) {
                    // 
                    return;
                }
                
                this.loadURLWithCache(this.menuUrl + "&page=" + (pageNum), this, this.onFetchResult, {
                    xmlparse: false,
                    pagenum: pageNum
                });
            } else {
            //    
            }  
        };
        
    }
    
    var _searchTwinkieXPathTable = {
        video: {
            thumb_url: "thumbnail-url",
            show_name: "show/name",
            show_id: "show/id",
            title: "title",
            season_number: "season-number", 
            episode_number: "episode-number", 
            programming_type: "programming-type", 
            rating: "rating",
            description: "description",
            duration: "duration",
            has_captions: "has-captions",
            video_id: "id",
            content_id: "content-id",
            pid: "pid"
        },
        show: {
            description: "description",
            genre: "genre",
            show_name: "name",
            show_id: "id",
            rating: "rating",
            votes_count: "votes-count",
            positive_votes_count: "positive-votes-count",
            company_name: "company/name",
            canonical_name: "canonical-name",
            title: "name"
        },
        company: {
            episode_count: "episode-count",
            is_hulu: "ishulu",
            thumb_url: "thumbnail-url",
            name: "show-name",
            company_id: "source-id",
            description: "description",
            full_episodes_count: "episode-count"
        }
    };
    
    function SearchCollection(options) {
        this.resultsCallback = null;
        this.resultsReceiver = null;
        this.asyncClosure = null;
        this.options = options;
        this.items = [];
        this.processedResults = {};
        this.typeCounts = {};

        this.getLength = function () {
            return this.typeCounts["video"] + this.typeCounts["show"] + this.typeCounts["company"];
        };
        
        this.getItemCount = function(itemType, videoType) {
            // TODO: HUTV-40 don't show empty video type filters
            return this.typeCounts[itemType];
        };
        
        this.getMetadataItem = function () {
            return null;
        };
        
        this.getItemAt = function (index) {
            if (index < this.items.length) {
                return {
                    success: true,
                    item: this.items[index]
                };
            }
            return {
                success: false,
                item: null
            };
        };
        
        this.setItemAt = function(index,item) {
            this.items[index] = item;
        };
        
        this.initialize = function (url, receiver, callback, asyncClosure) {
            this.resultsCallback = callback;
            this.resultsReceiver = receiver;
            this.asyncClosure = asyncClosure;
            this.typeCounts = {
                video: 0,
                show: 0,
                company: 0
            };
            
            // If no filter specified, or filtering on videos
            if (!this.options.item_type || this.options.item_type == "video") {
                url = _applyGlobalContext($htv.Constants.Endpoints.menu +
                    "/search?hulu_only=1&query=" + this.options.query +
                    "&items_per_page=" + $htv.Constants.DEFAULT_PAGE_SIZE + 
                    // Filter video type, if specified
                    (this.options.video_type ? "&type=" + this.options.video_type : ""));
                $htv.Platform.loadURL(url, this, this.onResultHelper, {
                    xmlparse: true,
                    singular: "video",
                    plural: "videos"
                });
            }
            // Otherwise mark it as already processed
            else {
                this.processedResults["video"] = [];
            }
            

            
            // If no filter specified, or filtering on shows
            if (!this.options.item_type || this.options.item_type == "show") {
            // test new search URL with data for web-only shows
            // e.g.  http://10.20.0.37:4001/search/search_show?query=haven&device_id=2&package_id=2
    		// url = "http://10.20.0.37:4001/search/search_show?query=" + this.options.query + "&device_id=2&package_id=2";
            
              url =  _applyGlobalContext($htv.Constants.Endpoints.menu 
                  + "/search?hulu_only=1&search_shows=1&query=" + this.options.query
                  + "&items_per_page=" + $htv.Constants.DEFAULT_PAGE_SIZE);
                $htv.Platform.loadURL(url, this, this.onResultHelper, {
                    xmlparse: true,
                    singular: "show",
                    plural: "shows"
                });
            }
            // Otherwise mark it as already processed
            else {
                this.processedResults["show"] = [];
            }
            
            // If no filter specified, or filtering on companies
            if (!this.options.item_type || this.options.item_type == "company") {
                url =  _applyGlobalContext($htv.Constants.Endpoints.menu
                    + "/search?hulu_only=1&search_companies=1&query=" + this.options.query
                    + "&items_per_page=" + $htv.Constants.DEFAULT_PAGE_SIZE);
                $htv.Platform.loadURL(url, this, this.onResultHelper, {
                    xmlparse: true,
                    singular: "company",
                    plural: "companies"
                });
            }
            // Otherwise mark it as already processed
            else {
                this.processedResults["company"] = [];
            }
        };
        
        this.onResultHelper = function (responseData, options) {
            var result, item, i, key, arr, meta, web_only_metadata;
            
            arr = [];
            
            result = $htv.Platform.evaluateXPath(responseData, "results/" + options.plural + "/" + options.singular);
            
            for (i = 0; i < result.length; i++) {
                item = $htv.Platform.getXPathResultAt(result, i);

                arr[i] = {};
                for (key in _searchTwinkieXPathTable[options.singular]) {
                    arr[i][key] = $htv.Platform.evaluateXPathString(item, _searchTwinkieXPathTable[options.singular][key]);
                }
                
                // Hard code child reference to page
                if (options.singular == "video") {
                    arr[i].items_url = "/menu/" + $htv.Constants.MENU_PREFIX + "video_page?video_id=" + arr[i].video_id;
                }
                else if (options.singular == "show") {
                    arr[i].items_url = "/menu/" + $htv.Constants.MENU_PREFIX + "show_page?show_id=" + arr[i].show_id;
                }
                else if (options.singular == "company") {
                    // Filter offsite companies (search bug: empty responses include 1 offsite company)
                    if (arr[i].is_hulu == "false") {
                        arr.pop();
                        continue;
                    }
                    
                    arr[i].items_url = "/menu/" + $htv.Constants.MENU_PREFIX + "company_page?company_id=" + arr[i].company_id;
                }
                
                // Record item type
                arr[i].item_type = options.singular;
            }
            
            // also interested in web-only results for shows
            if ([options.singular] == "show") {
                result = $htv.Platform.evaluateXPath(responseData, "results/site-only/site-only/show");
                web_only_metadata = $htv.Platform.evaluateXPath(responseData, "results/site-only/site-only");
            
                for (i = arr.length; i < result.length; i++) {
                    item = $htv.Platform.getXPathResultAt(result, i);
                    meta = $htv.Platform.getXPathResultAt(web_only_metadata, i);

                    arr[i] = {};
                    
                    for (key in _searchTwinkieXPathTable.show) {
                        arr[i][key] = $htv.Platform.evaluateXPathString(item, _searchTwinkieXPathTable[options.singular][key]);
                    }
                    
                    arr[i].web_only = true;
                    arr[i].web_only_message = $htv.Platform.evaluateXPathString(meta, "message").replace(/^[ \t]+/, "");
                    
                    arr[i].items_url = "/menu/" + $htv.Constants.MENU_PREFIX + "show_page?show_id=" + arr[i].show_id;
                    arr[i].item_type = "show";
                }
            }
            
            
            
            // Record item counts
            if (options.singular == "video") {
                this.typeCounts["video"] = arr.length;
                // TODO: HUTV-70 support more than one page of video results
                //this.typeCounts["video"] = $htv.Platform.evaluateXPathInt(responseData, "results/count");
            }
            else if (options.singular == "show") {
                this.typeCounts["show"] = arr.length;
            }
            else if (options.singular == "company") {
                this.typeCounts["company"] = arr.length;
            }
            
            this.processedResults[options.singular] = arr;
            
            if (this.processedResults["video"]
                && this.processedResults["show"]
                && this.processedResults["company"]) {
                this.onResultsComplete();
            }
        };
        
        this.onResultsComplete = function () {
            while (this.items.length > 0) {
                this.items.pop();
            }
            this.items = this.processedResults["show"].concat(this.processedResults["company"], this.processedResults["video"]);
            this.resultsCallback.call(this.resultsReceiver);
        };
    }
    
    //  TODO: we could have a request queue: allows for retries and such.
    var _activeMetadataRequest = null;

    function _makeMetadataRequestWithContentID(content_id, receiver, callback, optionalPackageId) {
       var url = _applyGlobalContext($htv.Constants.Endpoints.menu + "/videos?content_id=" + content_id, optionalPackageId);
        _makeMetadataRequest(url, receiver, callback, optionalPackageId);        
    }
    
    function _makeMetadataRequestWithVideoID(video_id, receiver, callback, optionalPackageId) {
        var url = _applyGlobalContext($htv.Constants.Endpoints.menu + "/videos?video_id=" + video_id, optionalPackageId);
        _makeMetadataRequest(url, receiver, callback, optionalPackageId);
    }
    
    function _makeMetadataRequest(url, receiver, callback, optionalPackageId) {
       // if (_activeMetadataReqest === null) {
        _activeMetadataRequest = {
            receiver: receiver,
            callback: callback,
            retry_count: 0,
            url: url
        };
        $htv.Platform.loadURL(url, this, onMetadataResponse, {
            xmlparse: true,
            errorCallback: onMetadataError
        });
    }
    
    function onMetadataResponse(responseData, options) {
        var results = $htv.Platform.evaluateXPath(responseData, "videos/video");
        var video = $htv.Platform.getXPathResultAt(results, 0);
        
        var result = {
            title: $htv.Platform.evaluateXPathString(video, "title"),
            description: $htv.Platform.evaluateXPathString(video, "description"),
            pid: $htv.Platform.evaluateXPathString(video, "pid"),
            segments: $htv.Platform.evaluateXPathString(video, "segments"),
            rating: $htv.Platform.evaluateXPathNumber(video, "rating"),
            content_id: $htv.Platform.evaluateXPathInt(video, "content_id"),
            video_id: $htv.Platform.evaluateXPathInt(video, "id"),
            has_captions: $htv.Platform.evaluateXPathString(video, "has_captions").toLowerCase() == "true",
            programming_type: $htv.Platform.evaluateXPathString(video, "programming_type"),
            duration: $htv.Platform.evaluateXPathInt(video, "duration"),
            thumb_url: $htv.Platform.evaluateXPathString(video, "thumbnail_url_16x9_large"),
            company_name: $htv.Platform.evaluateXPathString(video, "company_name"),
            programming_type: $htv.Platform.evaluateXPathString(video, "programming_type"),
            parent_channel_name: $htv.Platform.evaluateXPathString(video, "parent_channel_name"),
            end_credits_time: $htv.Platform.evaluateXPathString(video, "end_credits_time"),
            show_id: $htv.Platform.evaluateXPathInt(video, "show_id"),
            show_name: $htv.Platform.evaluateXPathString(video, "show_name"),
            show_canonical_name: $htv.Platform.evaluateXPathString(video, "show_canonical_name"),
            tune_in_information: $htv.Platform.evaluateXPathString(video, "tune_in_information"),
            copyright: $htv.Platform.evaluateXPathString(video, "copyright"),
            season_number: $htv.Platform.evaluateXPathInt(video, "season_number"),
            episode_number: $htv.Platform.evaluateXPathInt(video, "episode_number"),
            cp_identifier: $htv.Platform.evaluateXPathString(video, "cp_identifier")
        };
        _activeMetadataRequest.callback.call(_activeMetadataRequest.receiver, result);
    }
    
    function onMetadataError(status, errorData) {
        
        _activeMetadataRequest.callback.call(_activeMetadataRequest.receiver, {});
    }
    
    function _stringIsNullOrEmpty(str) {
        return str === null || str === undefined || String(str).replace(/\s/g, "").length === 0;
    }
    
    function _stripHTML(str) {
        if (str) {
            return str.replace(/<\S[^><]*>/g, "");
        }
        return str;
    }
    
    function _trim(str) {
        if (str) {
            return str.replace(/^\s+|\s+$/g, "");
        }
        return str;
    }
    
    function _getCaptionData(xmldoc) {
        var captions = [], results = null, i = 0;
        var samiString = null, bodyString = null, syncString = null;
        var testNode = null, travNode = null;
        
        // required due to differences in xml parsing
        travNode = $htv.Platform.getXMLRootNode(xmldoc);
        
        // verification and parsing
        if ($htv.Platform.getXMLNodeName(travNode).toLowerCase() == "sami") {
            samiString = $htv.Platform.getXMLNodeName(travNode);
            
        } else {
            
            return [];
        }
        
        for (i = 0; i < $htv.Platform.getXMLChildrenCount(travNode); i++) {
            testNode = $htv.Platform.getXMLChildAt(travNode, i);
            if (testNode && $htv.Platform.getXMLNodeName(testNode).toLowerCase() == "body") {
                bodyString = $htv.Platform.getXMLNodeName(testNode);
                
                travNode = testNode;
                break;
            }
        }
        
        if (bodyString === null) {
            
            return [];
        }
        
        for (i = 0; i < $htv.Platform.getXMLChildrenCount(travNode); i++) {
            testNode = $htv.Platform.getXMLChildAt(travNode, i);
            if (testNode && $htv.Platform.getXMLNodeName(testNode).toLowerCase() == "sync") {
                syncString = $htv.Platform.getXMLNodeName(testNode);
                
                travNode = testNode;
                break;
            }
        }
        
        if (syncString === null) {
            
            return [];
        }
        
        // XPATH (case insensitive?) to body/sync
        results = $htv.Platform.evaluateXPath(xmldoc, samiString + "/" + bodyString + "/" + syncString);
        if (results.length === 0) {
            
            return []; 
        }
        
        for (i = 0; i < results.length; i++) {
            var captionNode = $htv.Platform.getXPathResultAt(results, i);
            var time = $htv.Platform.evaluateXPathAttributeString(captionNode, "start");
            if (!time) {
                time = $htv.Platform.evaluateXPathAttributeString(captionNode, "START");
            }
            if (!time) {
                time = $htv.Platform.evaluateXPathAttributeString(captionNode, "Start");
            }
            if (time) {
                captions.push({
                    time: parseInt(time, 10),
                    encrypted: String($htv.Platform.evaluateXPathAttributeString(captionNode, "Encrypted")).toLowerCase(),
                    text: $htv.Platform.getXMLNodeText(captionNode)
                });
            }
            else {
                
            }
        }
        
        return captions;
    }
    
    function _urlEncode(str) {
        var output = '';
        var x = 0;
        if (_stringIsNullOrEmpty(str)) {
            str = "";
        }
        str = str.toString();
        var regex = /(^[a-zA-Z0-9_.]*)/;
        while (x < str.length) {
            var match = regex.exec(str.substr(x));
            if (match !== null && match.length > 1 && match[1] !== '') {
                output += match[1];
                x += match[1].length;
            } else {
                if (str[x] == ' ')
                    output += '+';
                else {
                    var charCode = str.charCodeAt(x);
                    var hexVal = charCode.toString(16);
                    output += '%' + ( hexVal.length < 2 ? '0' : '' ) + hexVal.toUpperCase();
                }
                x++;
            }
        }
        return output;
    }
    
    function _getAspectRatio(str) {
        if (str.indexOf("x") < 0) {
            return 16 / 9;
        }
        var ratio = str.split("x");
        return ratio[0] / ratio[1];
    }
    
    function _formatTimeRemaining(seconds) {
        var minutes = (seconds / 60) | 0;
        seconds = seconds % 60;
        
        var minutesSuffix = (minutes === 1) ? "" : "s";
        var secondsSuffix = (seconds === 1) ? "" : "s";
        
        var timeRemaining = "";
        if (minutes > 0) {
            timeRemaining += minutes + " minute" + minutesSuffix + " ";
        }
        
        timeRemaining += seconds + " second" + secondsSuffix;
        
        return timeRemaining;
    }

    function _parseDeejayResponse(response) {
        var i, initialization_vector, cipher_bytes, encrypted_bytes, decrypted_bytes, device_key_bytes, server_key_bytes, real_key_bytes, real_key, plainTextResponse;
        device_key_bytes = $htv.Libs.Crypto.util.hexToBytes(response.device_key);
        if (response.hasOwnProperty("server_key")) {
            server_key_bytes = $htv.Libs.Crypto.util.hexToBytes(response.server_key);
            real_key_bytes = new Array(server_key_bytes.length);
            if (device_key_bytes.length === server_key_bytes.length) {
                for (i = 0; i < device_key_bytes.length; i++) {
                    real_key_bytes[i] = device_key_bytes[i] ^ server_key_bytes[i];
                }
            }
        } else {
            real_key_bytes = device_key_bytes;
        }
        real_key = $htv.Libs.Crypto.util.bytesToHex(real_key_bytes);
        
        if ($htv.Platform.properties.weak_encryption === true) {            
            var jsonBlob = $htv.Platform.parseJSON(response.cipher_text);
            decrypted_bytes = $htv.Platform.decryptAES(jsonBlob.data, real_key, "00000000000000000000000000000000");
            var data = $htv.Platform.parseJSON($htv.Libs.Crypto.charenc.UTF8.bytesToString(decrypted_bytes));
            // patch in the data keys.
            jsonBlob.data = "";
            for (var key in data) {
                jsonBlob[key] = data[key];
            }
            return jsonBlob;
        } else {
            decrypted_bytes = $htv.Platform.decryptAES(response.cipher_text, real_key, "00000000000000000000000000000000", $htv.Platform.properties.has_gzip_decrypt);
            if ($htv.Platform.properties.has_gzip_decrypt) {
                plainTextResponse = decrypted_bytes;
            } else if ($htv.Platform.properties.has_gzip) {
                plainTextResponse = $htv.Libs.GZip.unzip(decrypted_bytes);
            } else {
                plainTextResponse = $htv.Libs.Crypto.charenc.UTF8.bytesToString(decrypted_bytes);
            }
            
            return $htv.Platform.parseJSON(plainTextResponse);
        }
    }
    
    // Parses dates in the following formats into local JS Date objects:
    //     Twinkie: 2010-10-04 06:45:00
    //     SiteAPI: 2010-10-09T19:02:23Z
    // Returns null if dateStr is not a valid string
    function _parseDate(dateStr) {
        if (_stringIsNullOrEmpty(dateStr)) {
            return null;
        }
        var dateTimeSplit, dateSplit, timeSplit, parsedDate;
        try {
            dateTimeSplit = dateStr.split(/[TZ ]/);
            dateSplit = dateTimeSplit[0].split("-");
            timeSplit = dateTimeSplit[1].split(":");
            parsedDate = new Date();
            // NOTE: JS months go from 0-11 but input strings go from 1-12
            parsedDate.setUTCFullYear(dateSplit[0], parseInt(dateSplit[1], 10) - 1, dateSplit[2]);
            parsedDate.setUTCHours(timeSplit[0], timeSplit[1], timeSplit[2]);
            return parsedDate;
        } catch (e) {
            return null;
        }
    }

    return {
        // public interface
        applyGlobalContext: _applyGlobalContext,
        copyObject: _copyObject,
        createGenericCollection: function (source_array) {
            return new GenericCollection(source_array);
        },
        createPosition: _createPosition,
        createTwinkieCollection: function (required_fields, page_size) {
            return new PagedTwinkieCollection(required_fields, page_size); 
        },
        createSearchCollection: function (options) {
            return new SearchCollection(options);
        },
        formatVideoSubtitle: _formatVideoSubtitle,
        formatSecondsAsTimeCode: _formatSecondsAsTimeCode,
        formatTimeRemaining: _formatTimeRemaining,
        getAspectRatio: _getAspectRatio,
        getCaptionData: _getCaptionData,
        getEndCreditsTimeFromString: _getEndCreditsTimeFromString,
        getURLForAsset: _getURLForAsset,
        getURLForChannelThumb: _getURLForChannelThumb,
        getURLForCompanyLogo: _getURLForCompanyLogo,
        getURLForCompanyThumb: _getURLForCompanyThumb,
        getURLForShowLogo: _getURLForShowLogo,
        getURLForShowThumb: _getURLForShowThumb,
        getURLForVideoThumb: _getURLForVideoThumb,
        makeMetadataRequestWithContentID: _makeMetadataRequestWithContentID,
        makeMetadataRequestWithVideoID: _makeMetadataRequestWithVideoID,
        parseDate: _parseDate,
        parseDeejayResponse: _parseDeejayResponse,
        stringIsNullOrEmpty: _stringIsNullOrEmpty,
        stripHTML: _stripHTML,
        trim: _trim,
        typeOf: _typeOf,
        urlEncode: _urlEncode
    };
}();
