//  HULU CONFIDENTIAL MATERIAL. DO NOT DISTRIBUTE.
//  Copyright (C) 2009-2010 Hulu, LLC
//  All Rights Reserved
/**
 *
 * htvPlatform.js
 *
 * Defines the set of APIs that are required for the core application to communicate
 * with the device-specific APIs. This includes rendering UI, controlling video playback,
 * evaluating XML/JSON, reading/writing local data, detecting system properties, and
 * receiving user input.
 */
/*jslint nomen: false, debug: true, evil: false, immed: false, plusplus: false*/

var $htv;

var DEBUG_MODE = false;
var DEBUG_ONSCREEN = false;

function LOG(msg) {
    if (DEBUG_ONSCREEN === true) {
        LOG_ONSCREEN(msg);
    }
    else if (DEBUG_MODE === true) {
        alert(msg);
    }
}

var _consoleLabel = null;
function LOG_ONSCREEN(msg) {
    if (_consoleLabel === null) {
        try {
            _consoleLabel = $htv.Platform.createLabel({
                x: 10,
                y: 30,
                z: 10,
                width: 600,
                height: 800
            }, "Console:", null, {
                styleName: "white12arial"
            });
        }
        catch(e) { return; }
    }

    var oldText = _consoleLabel.getText();
    if (oldText.length > 1000) {
        _consoleLabel.setText(oldText.substr(100) + "<BR/>" + String(msg).substr(0,100));
    } else {
        _consoleLabel.setText(oldText + "<BR/>" + String(msg).substr(0,100));
    }    
}

function describe(obj) {
    
    
    for (key in obj) {
        
    }
    
}

// a buggy debug method
function getStack(){
    var fnRE  = /function\s*([\w\-$]+)?\s*\(/i;
    var caller = arguments.callee.caller;
    var stack = "Stack = ";
    var fn;
    
    var iterCnt = 0;
    while (caller && iterCnt < 10) {
        fn = fnRE.test(caller.toString()) ? RegExp.$1 || "{?}" : "{?}";
        stack += "-->" + fn;
        caller = caller.arguments.callee.caller;
        iterCnt++;
    }
    
}

$htv.Platform = function () {
    // Platform Specific Globals
    
    var __stage,
        _segmentIndex,
        _origURL,
        __playerCallback,
        __playerReceiver,
        __eventCallback,
        __eventReceiver,
        _userMuteState,
        _playerPaused,
        _pluginMW,
        _pluginPlayer,
        _pluginAudio,
        _pluginNetwork,
        _pluginNNavi,
        _pluginTV,
        _pluginAPI,
        _widgetAPI,
        _originalMWSource,
        _lastPlayerTime,
        _pendingSeekTime,
        _fileSystem,
        _persistDict,
        _activeObjects,
        _currentBandwidth,
        _lastKeydown,
        _pluginWindow,
        _currentPlayerPosition,
        _firstProgressEvent,
        _persistDictChanged,
        _flushDataTimer,
        _bufferingDaemonEnabled,
        _minimizeMaximizeHackEnabled,
        _resumePlayHackStageOneEnabled,
        _resumePlayHackStageTwoEnabled,
        _resumePlayHackStageThreeEnabled,
        _bufferingDaemon,
        _defaultBitrate,
        _isSeeking,
        _bufferingWindowPeriod,                 // Period over which we observe (measured against video progress).
        _bufferingThreshold,                    // Number of buffering events needed to trigger response.
        _bufferingEvents,                       // Holds buffering events within the sliding window.
        _cumulativeTimeSinceLastBufferingEvent, // Time (measured against video progress) watched since the last buffering event.
        _ignoreNextBufferingEvent;              // Ignore the next buffering start and complete events (since they're anticipated).

    __stage = null;
    _segmentIndex = 0;
    _origURL = "";
    _activeObjects = 0;
    __playerReceiver = null;
    __playerCallback = null;
    __eventReceiver = null;
    __eventCallback = null;
    _userMuteState = null; // Since we receive all the mute state changes, we'll maintain it locally to save an API call.
    // hack list: 
    _bufferingDaemonEnabled = false;
    _minimizeMaximizeHackEnabled = false;
    _resumePlayHackStageOneEnabled = false;
    _resumePlayHackStageTwoEnabled = false;
    _resumePlayHackStageThreeEnabled = false;
    _bufferingDaemon = {timer: _createTimer(0, null, null),
                        count: 0,
                        monitoring: false}; 
    _originalMWSource = null; // initialize to null
    _defaultBitrate = "3200";
    
    _isSeeking = false;
    _bufferingEvents = [];
    _cumulativeTimeSinceLastBufferingEvent = 0;
    _ignoreNextBufferingEvent = false;
    _currentPlayerPosition = {
        x: 0,
        y: 0,
        width: 960,
        height: 540
    };
    
        
    // attachEventHandlers
    //   
    //  attaches keyboard events to our api keypresshandler, which ends up calling takeuseraction
    // 
    function _attachEventHandlers(receiver, callback, player_receiver, player_callback) {
        // Focus the anchor div to enable key presses
        document.getElementById("anchor").focus();
        
        if (_widgetAPI === null || _widgetAPI === undefined) {
            _widgetAPI = new Common.API.Widget();
        }
        if (_pluginAPI === null || _pluginAPI === undefined) {
            _pluginAPI = new Common.API.Plugin();
        }
        
        // _initPictureSettings();
       
        // Once focus is applied, send ready event.
        _widgetAPI.sendReadyEvent();

        __eventReceiver = receiver;
        __eventCallback = callback;
        __playerReceiver = player_receiver;
        __playerCallback = player_callback;
    }
    
    function _initPictureSettings() {
        if (!_isEmulator()) {
            var url_query = decodeURIComponent(window.location.search);
            var tmpStrArr = new Array();
            var tmpStrArr_detail = new Array();
            var curLang;
            tmpStrArr = url_query.split("&");
            for (var i = 0; i < tmpStrArr.length; i++) {
                tmpStrArr_detail = tmpStrArr[i].split("=");
                
                if (tmpStrArr_detail[0] == "language") {
                    curLang = new Common.Util.Language().convertCodeToKeyword(parseInt(tmpStrArr_detail[1]));
                    if (CM_AVSetting) {
                        CM_AVSetting.initAVSetting(curLang, function() {
                            
                            if (bContentsList) {
                                
                            } else {
                                LOG("lastFocusID :: " + lastFocusID)
                            }
                        });
                    }
                }
            }
        }
    }
  
    function _createSpacer(position, canvas, options) {
        // only pays attention to width/height.
        var spacer = document.createElement("div");
        spacer.style.left = position.x;
        spacer.style.top = position.y;
        spacer.style.zIndex = position.z;
        spacer.style.height = 1;
        spacer.style.width = 1;
        spacer.style.background = 'transparent';
        // extremely useful for spacer debugging.
        // spacer.style.backgroundColor ='rgb(' + Math.floor(Math.random()*255) + ',' + Math.floor(Math.random()*255) + ',' + Math.floor(Math.random()*255) + ')';
        spacer.style.paddingLeft = position.width;
        spacer.style.paddingTop = position.height; 
        document.body.appendChild(spacer);
        return {
            handle: spacer,
            container: canvas,
            type: "spacer",
            position: position,
            resize: function (newSize) {
                this.handle.style.paddingLeft = newSize.width;
                this.handle.style.paddingTop = newSize.height; 
                this.handle.parentNode.replaceChild(this.handle,this.handle);
            },
            hide: function() {
                _hideItem(this.handle);
            },
            show: function() {
                _showItem(this.handle);
            }
        };
    }
    
    function _createContainer(position, canvas, options) {
        var box = document.createElement("div");
        box.style.left = position.x;
        box.style.top = position.y;
        box.style.zIndex = position.z;
        box.style.height = position.height;
        box.style.width = position.width;
        box.style.background = 'transparent';
        box.style.overflow = 'hidden';
        box.style.position = 'absolute';
        var verticalMode = false;
        if (options && options.hasOwnProperty("vertical")) {
            verticalMode = options.vertical;
        }
        var spacing = 10;
        if (options && options.hasOwnProperty("spacing")) {
            spacing = options.spacing + "px";
        }
        var edgePadding = 10;
        if (options && options.hasOwnProperty("edgePadding")) {
            edgePadding = options.edgePadding + "px";
        }
        var centerMode = false;
        if (options && options.hasOwnProperty("center")) {
            centerMode = options.center;
        }
        
        if (verticalMode) {
            box.style.paddingTop = edgePadding;
        } else {
            box.style.paddingLeft = edgePadding;
        }
        
        if (centerMode) {
            box.style.textAlign = 'center';
        } 
        document.body.appendChild(box);
        return {
            handle: box,
            vertical: verticalMode,
            center: centerMode,
            position: position,
            spacing: spacing,
            addItem: function(item, options) {
                if (this.center) {
                    item.handle.style.display = 'inline-block';
                } else {
                    item.handle.style.cssFloat = 'left';
                }
                if (options && options.hasOwnProperty("padding")) {
                    item.handle.style.paddingTop = (options.padding.top || 0) + "px";
                    item.handle.style.paddingLeft = (options.padding.left || 0) + "px";
                    item.handle.style.paddingRight = (options.padding.right || 0) + "px";
                    item.handle.style.paddingBottom = (options.padding.bottom || 0) + "px";
                }
                // If removing the last item, update the new last item's spacing
                if (this.vertical) {
                    item.handle.style.height = 'auto';
                    item.handle.style.position = 'static';
                    item.handle.style.marginBottom = "0px";
                    if (this.handle.childNodes.length > 0) {
                        this.handle.childNodes[this.handle.childNodes.length - 1].style.marginBottom = this.spacing;
                    }
                } else {
                    item.handle.style.width = 'auto';
                    item.handle.style.position = 'static';
                    item.handle.style.marginRight = "0px";
                    if (this.handle.childNodes.length > 0) {
                        this.handle.childNodes[this.handle.childNodes.length - 1].style.marginRight = this.spacing;
                    }
                }
                if (item.handle.parentNode) {
                    item.handle.parentNode.removeChild(item.handle);
                }
                this.handle.appendChild(item.handle);
            },
            addItemAtIndex: function(item, index, options) {
                if (this.center) {
                    item.handle.style.display = 'inline-block';
                } else {
                    item.handle.style.cssFloat = 'left';
                }  
                if (options && options.hasOwnProperty("padding")) {
                    item.handle.style.paddingTop = (options.padding.top || 0) + "px";
                    item.handle.style.paddingLeft = (options.padding.left || 0) + "px";
                    item.handle.style.paddingRight = (options.padding.right || 0) + "px";
                    item.handle.style.paddingBottom = (options.padding.bottom || 0) + "px";
                }
                if (this.vertical) {
                    item.handle.style.height = 'auto';
                    item.handle.style.position = 'static';
                    item.handle.style.marginBottom = this.spacing;
                } else {
                    item.handle.style.width = 'auto';
                    item.handle.style.position = 'static';
                    item.handle.style.marginRight = this.spacing;
                }
                if (item.handle.parentNode) {
                    item.handle.parentNode.removeChild(item.handle);
                }
                
                if (index === 0) {
                    // Hack around the "0-index" bug.
                    this.handle.insertBefore(item.handle, this.handle.firstChild);
                }
                else {
                    this.handle.insertBefore(item.handle, this.handle.childNodes[index]);
                }
            },
            containsItem: function(item) {
                return this.getIndexOfItem(item) !== -1;
            },
            removeItemAtIndex: function(index) {
                // If removing the last item, update the new last item's spacing
                if (index == this.handle.childNodes.length - 1
                    && this.handle.childNodes.length > 2) {
                    if (this.vertical) {
                        this.handle.childNodes[this.handle.childNodes.length - 2].style.marginBottom = "0px";
                    }
                    else {
                        this.handle.childNodes[this.handle.childNodes.length - 2].style.marginRight = "0px";
                    }
                }

                // TODO: a garbage div to hold orphans?
                var itemHandle = this.handle.childNodes[index];
                this.handle.removeChild(itemHandle);
                itemHandle.style.visibility = 'hidden';
                document.body.appendChild(itemHandle);
            },
            getIndexOfItem: function(item) {
                for (var i = 0; i < this.handle.childNodes.length; i++) {
                    if (this.handle.childNodes[i] == item.handle) {
                        return i;
                    }
                }
                return -1;
            },
            contentsOverflowed: function() {
                return (this.handle.clientHeight < this.handle.scrollHeight);
            },
            removeItem: function(item) {
                var index = this.getIndexOfItem(item);
                if (index >= 0) {
                    this.removeItemAtIndex(index);
                }
            },
            hide: function(){
                _hideItem(this.handle);
            },
            show: function(){
                _showItem(this.handle);
            },
            getPosition: function() {
                return this.position;
            },
            setPosition: function(position) {
                this.position = position;
                _moveItem(this.handle, position, duration);
            },
            move: function(position, duration){
                this.position = position;
                _moveItem(this.handle, position, duration);
            }
        }
    }
    
    var _textDimensionCache = $htv.Libs.JSONparse('{"gray18futura_Most Popular": {"textWidth":109,"textHeight":22},"gray18futura_Recently Added":{"textWidth":124,"textHeight":22},"gray18futura_Browse TV":{"textWidth":87,"textHeight":22},"gray18futura_Queue / Profile":{"textWidth":131,"textHeight":22},"puke18futura_Most Popular":{"textWidth":109,"textHeight":22},"white18futura_Queue / Profile":{"textWidth":131,"textHeight":22},"white18futura_Recently Added":{"textWidth":124,"textHeight":22},"white18futura_Recommended":{"textWidth":122,"textHeight":22},"puke18futura_Queue / Profile":{"textWidth":131,"textHeight":22},"white18futura_Most Popular":{"textWidth":109,"textHeight":22},"white18futura_Help":{"textWidth":39,"textHeight":22},"puke18futura_Recommended":{"textWidth":122,"textHeight":22},"white18futura_":{"textWidth":0,"textHeight":0},"puke18futura_Help":{"textWidth":39,"textHeight":22},"white18futura_Browse TV":{"textWidth":87,"textHeight":22},"white18futura_Browse Movies":{"textWidth":125,"textHeight":22},"puke18futura_Recently Added":{"textWidth":124,"textHeight":22},"white18futura_Search":{"textWidth":55,"textHeight":22},"puke18futura_Browse TV":{"textWidth":87,"textHeight":22},"puke18futura_Browse Movies":{"textWidth":125,"textHeight":22},"puke18futura_Search":{"textWidth":55,"textHeight":22}}');
    
    // API rendering methods
    function _createLabel(position, text, canvas, options) {
        var tf = document.createElement("div");
        _widgetAPI.putInnerHTML(tf, _convertLabelText(text));
        tf.style.left = position.x;
        tf.style.top = position.y;
        tf.style.zIndex = position.z;
        tf.style.height = position.height;
        tf.style.width = position.width;
        
        if (!options) {
            options = {};
        }
        // for styling
        if (options.hasOwnProperty("styleName") && options.styleName) {
            tf.setAttribute("class", options.styleName);
        }
        
        if (options.center === true) {
            tf.style.textAlign = "center";
        }
        tf.style.background = 'transparent';
        
        document.body.appendChild(tf);
        _activeObjects++;
        return {
            handle: tf,
            container: canvas,
            style: options.styleName,
            position: position,
            hide: function(){
                _hideItem(this.handle);
            },
            show: function(){
                _showItem(this.handle);
            },
            getTextDimensions: function(){ // please don't call this method unless you have to. it's slow.
                if (!_textDimensionCache.hasOwnProperty(this.style + "_" + this.handle.innerHTML)) {
                    // SUPER DUPER WEIRD: if you don't access the offsetHeight value here it doesn't work
                    var temp = this.handle.offsetHeight;
                    this.handle.style.width = '';
                    this.handle.style.height = '';
                    var offsetWidth = parseInt(this.handle.offsetWidth);
                    var offsetHeight = parseInt(this.handle.offsetHeight);
                    this.handle.style.width = this.position.width;
                    this.handle.style.height = this.position.height;
                    _textDimensionCache[this.style + "_" + this.handle.innerHTML] = {
                        textWidth: offsetWidth,
                        textHeight: offsetHeight
                    };
                    
                }
                return _textDimensionCache[this.style + "_" + this.handle.innerHTML];
            },
            getText: function(){
                return this.handle.innerHTML;
            },
            setText: function(text){
                _widgetAPI.putInnerHTML(this.handle, _convertLabelText(text));
            },
            getPosition: function() {
                return this.position;
            },
            setPosition: function(position) {
                this.position = position;
                _moveItem(this.handle, position, duration);
            },
            setStyle: function(styleName) {
                this.style = styleName;
                if (styleName) {
                    this.handle.setAttribute("class", styleName);
                }
            },
            move: function(position, duration){
                this.position = position;
                _moveItem(this.handle, position, duration);
            }
        };
    }
    
    // Make spaces (one or many consecutive) work in divs
    function _convertLabelText(obj) {
        if (obj) {
            return String(obj).replace(/  /g, "&nbsp;&nbsp;").replace(/\(c\)/g, "&copy;");
        }
        return "";
    }
    
    // Uses _convertLabelText, plus makes one or many consecutive line breaks work 
    function _convertTextAreaText(obj) {
        if (obj) {
            return _convertLabelText(obj).replace(/\n/g, "<br/>").replace(/<br\/><br\/>/g, "<br/>&nbsp;<br/>");
        }
        return "";
    }
    
    // todo: are TA's labels just like in lithium?
    function _createTextArea(position, text, canvas, options) {
        var ta = document.createElement("div");
        _widgetAPI.putInnerHTML(ta, _convertTextAreaText(text));
        ta.style.left = position.x;
        ta.style.top = position.y;
        ta.style.zIndex = position.z;
        ta.style.height = position.height;
        ta.style.width = position.width;
        ta.style.background = 'transparent';
        
        if (options && options.styleName) {
            ta.setAttribute("class", options.styleName);
        }
        
        if (options && options.center) {
            if (options.center === true) {
                ta.style.textAlign = "center";
            }
        }
        
        document.body.appendChild(ta);
        _activeObjects++;
        
        return {
            handle: ta,
            container: canvas,
            position: position,
            hide: function(){
                _hideItem(this.handle);
            },
            show: function(){
                _showItem(this.handle);
            },
            getPosition: function() {
                return this.position;
            },
            setPosition: function(position) {
                this.position = position;
                _moveItem(this.handle, position, duration);
            },
            getText: function(){
                return this.handle.innerHTML;
            },
            setText: function(text){
                _widgetAPI.putInnerHTML(this.handle, _convertTextAreaText(text));
            },
            getTextDimensions: function(){ // please don't call this method unless you have to. it's slow.
                // SUPER DUPER WEIRD: if you don't access the offsetHeight value here it doesn't work
                var temp = this.handle.offsetHeight;
                
                this.handle.style.width = '';
                this.handle.style.height = '';
                var offsetWidth = parseInt(this.handle.offsetWidth);
                var offsetHeight = parseInt(this.handle.offsetHeight);
                this.handle.style.width = this.position.width;
                this.handle.style.height = this.position.height;
                return {
                    textWidth: offsetWidth,
                    textHeight: offsetHeight
                };
            },
            setStyle: function(styleName) {
                if (styleName) {
                    this.handle.setAttribute("class", styleName);
                }
            },
            move: function(position, duration){
                this.position = position;
                _moveItem(this.handle, position, duration);
            }
        };
    }
    
    function _createImage(position, url, canvas, options) {
        var img = document.createElement("img");
        img.style.left = position.x;
        img.style.top = position.y;
        img.style.zIndex = position.z;
        img.style.width = position.width;
        img.style.height = position.height;
        img.src = url;
        if (options && options.styleName) {
            img.setAttribute("class", options.styleName);
        }
        if (options && options.hasOwnProperty("maxHeight")) {
            img.style.maxHeight = options.maxHeight + "px";
        }
        if (options && options.hasOwnProperty("maxWidth")) {
            img.style.maxWidth = options.maxWidth + "px";
        }
        
        document.body.appendChild(img);
        _activeObjects++;

        return {
            handle: img,
            position: position,
            container: canvas,
            hide: function(){
                _hideItem(this.handle);
            },
            show: function(){
                _showItem(this.handle);
            },
            move: function(position, duration){
                this.position = position;
                _moveItem(this.handle, position);
            },
            getPosition: function() {
                return this.position;
            },
            setPosition: function(position) {
                this.position = position;
                _moveItem(this.handle, position, duration);
            },
            setStyle: function(styleName) {
                if (styleName) {
                    this.handle.setAttribute("class", options.styleName);
                }
            },
            setURL: function(url) {
                if (this.handle.src !== url) {
                    this.handle.src = url;
                }
            }
        };
    }
    
    function _applyBoxStyle(box, style) {
        switch(style) {
        case "PlayerProgressEmpty":
            box.style.backgroundImage = "url('images/player-progress-empty.png')";
            box.style.backgroundRepeat = "repeat-x";
            break;
        case "PlayerProgressFull":
            box.style.backgroundImage = "url('images/player-progress-full.png')";
            box.style.backgroundRepeat = "repeat-x";
            break;
        case "PlayerProgressBar":
            box.style.backgroundImage = "url('images/player-progress-topbottom-bar.png')";
            box.style.backgroundRepeat = "repeat-x";
            break;
        case "NonSlidingMenuSelectionHighlight":
            box.style.backgroundImage = "url('http://nick.corp.hulu.com:8082/highlightImage?width=" + box.width + "&height=" + box.height + "')";
            break;
        case "VideoProgressBackground":
            box.style.backgroundColor = "#161616";
            break;
        case "VideoProgressEmpty":
            box.style.backgroundColor = "#3B3B3B";
            break;
        case "VideoProgressFilled":
            box.style.backgroundImage = "url('images/video_progress_gradient.png')";
            box.style.backgroundRepeat = "repeat-x";
            break;
        case "ResumeBackgroundBorder":
            box.style.backgroundColor = "#5c5c5c";
            break;
        case "ResumeProgressEmpty":
            box.style.backgroundImage = "url('images/resume-progress-bar-empty.jpg')";
            box.style.backgroundRepeat = "repeat-x";
            break;
        case "ResumeProgressFilled":
            box.style.backgroundImage = "url('images/resume-progress-bar-fill.jpg')";
            box.style.backgroundRepeat = "repeat-x";
            break;
        case "VolumeIndicatorFilled":
            box.style.backgroundImage = "url('images/volume-fill.jpg')";
            box.style.backgroundRepeat = "repeat-y";
            break;
        case "CountdownBackground":
            box.style.backgroundColor = "transparent"; // default divs are black background, shows through the alpha
            box.style.backgroundImage = "url('images/countdown_background.png')";
            box.style.backgroundRepeat = "repeat";
            break;
        case "AlertDialogVeil":
            box.style.backgroundColor = "transparent"; // default divs are black background, shows through the alpha
            box.style.backgroundImage = "url('images/alert-dialog-veil.png')";
            box.style.backgroundRepeat = "repeat";
            break;
        case "NextVideoBorder":
            box.style.backgroundColor = "#2A2A2A";
            break;
        case "AlertDialogBoxInner":
			box.style.backgroundColor = "#000000";
			break;
		case "AlertDialogBoxOuter":
			box.style.backgroundColor = "#3B3B3B";
			break;
        default:
            box.style.backgroundImage = "url('images/nogradient.jpg')";
            box.style.backgroundRepeat = "repeat";
            break;
        }
    }
    
    
    function _createBox(position, style, canvas, options) {
        var box = document.createElement("div");
        box.style.left = position.x;
        box.style.top = position.y;
        box.style.zIndex = position.z;
        box.style.width = position.width;
        box.style.height = position.height;
        document.body.appendChild(box);
        _applyBoxStyle(box, style);
        
        return {
            handle: box,
            container: canvas,
            hide: function(){
                _hideItem(this.handle);
            },
            show: function(){
                _showItem(this.handle);
            },
            setZIndex: function(index){
                this.handle.style.zIndex = index;
            },
            move: function(position, duration){
                _moveItem(this.handle, position);
                _applyBoxStyle(this.handle, style);
            },
            setStyle: function(styleName) {
                _applyBoxStyle(this.handle, styleName);
            }
        };
    }
    
    function _createTimer(interval, receiver, callback) {
        var timerObj = {
            handle: -1,
            receiver: receiver,
            callback: callback,
            interval: interval
        };
        timerObj.start = function() {
            timerObj.stop();
            timerObj.handle = setInterval(
                function() { timerObj.callback.call(timerObj.receiver); }, 
                timerObj.interval);
        };
        timerObj.stop = function() {
            if (timerObj.handle != -1) {
                clearInterval(timerObj.handle);
                timerObj.handle = -1;
            }
        };
        timerObj.setCallback = function(newReceiver, newCallback) {
            timerObj.receiver = newReceiver;
            timerObj.callback = newCallback;

            // If active, restart
            if (timerObj.handle != -1) {                
                timerObj.stop();
                timerObj.start();
            }
        };
        timerObj.setInterval = function(newInterval) {
            timerObj.interval = newInterval;
            
            // If active, restart
            if (timerObj.handle != -1) {
                timerObj.stop();
                timerObj.start();
            }
        };
        timerObj.destroy = function() {
            timerObj.stop();
            timerObj.receiver = null;
            timerObj.callback = null;
        };
        timerObj.running = function() {
            return timerObj.handle !== -1;
        };
        return timerObj;
    }
    
    function _stringToXMLDoc(text){
        var parser, doc;
        doc = null;
        parser = new DOMParser();
        try {
            doc = parser.parseFromString(text, "text/xml");
        } 
        catch (e) {
            
        }
        return doc;
    }
    
    function _hideItem(item){
        if (item === null) {
            
            getStack();
        }
        else {
            item.style.visibility = 'hidden';
        }
    }
    
    function _showItem(item){
        if (item === undefined || item === null) {
            
            getStack();
        }
        else {
            item.style.visibility = 'visible';
        }
    }
    
    function _moveItem(item, position, duration){
        if (item != null) {
            item.style.left = position.x;
            item.style.top = position.y;
            item.style.zIndex = position.z;
            item.style.height = position.height;
            item.style.width = position.width;
        }
    }
    
    function _deleteItem(item){
        if (null != item) {
            //TODO: delete item;
            item.hide();
            if (item.handle.parentNode) {
                item.handle.parentNode.removeChild(item.handle);
            }
            _activeObjects--;
            item.handle = null;
        }
    }

    function _evaluateXPath(xml, xpath){
        var splitXpath = xpath.split("/");
        var results = new Array();
        // Start by getting all nodes with the leaf nodename
        var candidates = xml.getElementsByTagName(splitXpath[splitXpath.length - 1]);
        
        // For each candidate node
        for (var i = 0; i < candidates.length; i++) {
            var candidate = candidates[i];
            var parentTraversal = candidate.parentNode;
            
            // Traverse up from leaf nodename to root to confirm xpath
            for (var j = splitXpath.length - 2; j >= 0; j--) {
                if (parentTraversal == null || parentTraversal.nodeName != splitXpath[j]) {
                    candidate = null;
                    break;
                }
                parentTraversal = parentTraversal.parentNode;
            }
            
            // If made it to root, save candidate
            if (candidate) {
                results.push(candidate);
            }
        }
        
        return results;
    }
    
    function _evaluateXPathInt(xml, xpath){
        var result = parseInt(_evaluateXPathString(xml, xpath));
        return isNaN(result) ? 0 : result;
    }
    
    function _evaluateXPathNumber(xml, xpath){
        var result = Number(_evaluateXPathString(xml, xpath));
        return isNaN(result) ? 0 : result;
    }
    
    function _evaluateXPathString(xml, xpath){
        try {
            var results = _evaluateXPath(xml, xpath);
            if (results
                && results.length > 0
                && results[0].firstChild) {
                return results[0].firstChild.data;
            }
        }
        catch (e) {
            
        }
        return "";
    }
    
    function _evaluateXPathAttributeString(xml, attributeName){
        try {
            var attr = xml.getAttribute(attributeName);
            if (attr !== null) {
                return attr;
            }
        }
        catch (e) {
            
        }
        return "";
    }
    
    function _exit(){
        if (_pluginPlayer) {
            _pluginPlayer.Stop();
        }
        if (_widgetAPI) {
            _widgetAPI.sendReturnEvent();
        }
    }
    
    function _getXPathResultAt(xpathResults, index) {
        return xpathResults[index];
    }
    
    function _getXMLChildAt(xmlnode, index){
        try {
            return xmlnode.childNodes[index];
        }
        catch (e) {
            
        }
        return null;
    }
    
    function _getXMLChildrenCount(xmlnode) {
        try {
            return xmlnode.childNodes.length;
        }
        catch (e) {
            
        }
        return 0;
    }
    
    function _getXMLNodeName(xmlnode) {
        try {
            return xmlnode.nodeName;
        }
        catch (e) {
            
        }
        return "";
    }
    
    // gets text contents at current xml node
    function _getXMLNodeText(xmlnode) {
        try {
            //return xmlnode.firstChild.nodeValue;
            return xmlnode.childNodes[0].nodeValue;
        }
        catch (e) {
            
        }
        return "";
    }
    
    function _getXMLRootNode(xmldoc) {
        return _getXMLChildAt(xmldoc, 0);
    }
    
    function _parseJSON(jsonData) {
        return $htv.Libs.JSONparse(jsonData);
    }
    
    function _stringifyJSON(jsonData) {
        return $htv.Libs.JSONstringify(jsonData);
    }
    
    // Entry point for device down events
    function _keyDownHandler() {
        // NOTE: consecutive is only used platform-internally to determine the first
        // hold event. is_hold is used externally to know if it's a tap or hold gesture
        
        var consecutive = 1;
        if (_lastKeydown
            && _lastKeydown.key_code == event.keyCode) {
            
            // Increment the consecutive counter
            consecutive = _lastKeydown.consecutive + 1;
            
            // Send the hold event if didn't already send it
            if (consecutive == 2) {
                _keyPressHandler({
                    key_code: event.keyCode,
                    is_down: true, 
                    is_hold: true
                });
            }
        }
        
        var eventData = {
            key_code: event.keyCode,
            is_down: true,
            is_hold: false,
            consecutive: consecutive
        };

        // Always send the keydown for tap
        _keyPressHandler(eventData);
        _lastKeydown = eventData;
    }
    
    // Entry point for device up events
    function _keyUpHandler() {
        // Always send the keyup event. Use the last key even tot
        _keyPressHandler({
            key_code: event.keyCode,
            is_down: false,
            // Infer up's hold state from the number of consecutive down events
            is_hold: (_lastKeydown && _lastKeydown.consecutive > 1)
        });
        _lastKeydown = null;
    }

    // Translates keydown/keyup events from the device
    // into the platform. Receives eventData and passes
    // it on to the core in the following format:
    //    eventData = {
    //      key_code: int indicating device key code
    //      is_down: boolean indicating whether event is a keydown or keyup
    //      is_hold: boolean indicating whether two down events have just occurred 
    //      action: string indicating the translated key event (mapped in _keyPressHandler)
    //      consecutive: int indicating how many system down events have occurred since the last up (used platform-internally)
    //    }
    function _keyPressHandler(eventData) {
        var action = null;
        switch(eventData.key_code) {
            case tvKey.KEY_PLAY:
                action = "PLAY";
                break;
            case tvKey.KEY_RED: // red is same as stop, specced
            case tvKey.KEY_STOP:
                action = "STOP";
                break;
                
            case tvKey.KEY_PAUSE:
                action = "PAUSE";
                break;
            case tvKey.KEY_INFO:
            case tvKey.KEY_INFOLINK:
                action = "KEY_INFO";
                break;
            case tvKey.KEY_SUBTITLE:
            case tvKey.KEY_SUB_TITLE:
                action = "KEY_SUBTITLE";
                break;
            case tvKey.KEY_EXIT:
                action = "KEY_EXIT";
                _widgetAPI.blockNavigation(event);
                break;
            case tvKey.KEY_FF:
                action = "KEY_FF";
                break;
            case tvKey.KEY_FF_:
                action = "KEY_FF_JUMP";
                break;
            case tvKey.KEY_RW:
                action = "KEY_RW";
                break;
            case tvKey.KEY_REWIND_:
                action = "KEY_RW_JUMP";
                break;
            case tvKey.KEY_VOL_UP:
            case tvKey.KEY_PANEL_VOL_UP:
                action = "VOLUME_UP";
                break;
                
            case tvKey.KEY_VOL_DOWN:
            case tvKey.KEY_PANEL_VOL_DOWN:
                action = "VOLUME_DOWN";
                break;
            case tvKey.KEY_MUTE:
                action = "VOLUME_MUTE";
                break;
            case tvKey.KEY_CH_UP: 
            case tvKey.KEY_PANEL_CH_UP:
                action = "CHANNEL_UP";
                break;
            case tvKey.KEY_CH_DOWN:    
            case tvKey.KEY_PANEL_CH_DOWN:
                action = "CHANNEL_DOWN";
                break;
            case tvKey.KEY_LEFT:
                action = "MOVE_LEFT";
                break;
                
            case tvKey.KEY_RIGHT:
                action = "MOVE_RIGHT";
                break;
                
            case tvKey.KEY_UP:
                action = "MOVE_UP";
                break;

            case tvKey.KEY_DOWN:
                action = "MOVE_DOWN";
                break;

            case tvKey.KEY_ENTER:
            case tvKey.KEY_PANEL_ENTER:
                action = "PRESS_OK";
                break;
                
            
            case tvKey.KEY_GREEN:
                action = "KEY_GREEN";
                break;
                
            case tvKey.KEY_D_VIEW_MODE:
            case tvKey.KEY_BLUE:
                action = "PLAYER_TOGGLE";
                break;
            case tvKey.KEY_YELLOW:
                action = "KEY_YELLOW";
                break;
            case tvKey.KEY_RETURN:
                action = "KEY_RETURN";
                _widgetAPI.blockNavigation(event);
                break;
/*
            case tvKey.KEY_MENU:
                if (!_isEmulator()) {
                    action = "KEY_MENU";
                    if (CM_AVSetting.bBDPlayer == false) {
                        CM_AVSetting.showMenuPopup();
                    } else {
                        
                    }
                }
*/
            default:
                
                break;
        }
        if (null != action) {
            
            eventData.action = action;
            try {
                __eventCallback.call(__eventReceiver, "USER_INPUT", eventData);
            } catch (e) {
                
                describe(e);
            }
        }
    }
    
    function _bootstrap(options) {
        var localDataFile;
        _fileSystem = new FileSystem();
        _persistDict = {};
        
        if (_fileSystem.isValidCommonPath(curWidget.id) == 0) {
            _fileSystem.createCommonDir(curWidget.id);
        }
        localDataFile = _fileSystem.openCommonFile(curWidget.id + "/huludata.dat", "r");
        
        if (localDataFile !== null) {
            
            var fileContents = localDataFile.readAll();
            
            try {
                _persistDict = _parseJSON(fileContents);
            } 
            catch (e) {
                
            }
            describe(_persistDict);
            _fileSystem.closeCommonFile(localDataFile);
        }
        else {
            
        }
        _persistDictChanged = false;
        _runSpeedTest();
        
        if (_pluginAPI === null || _pluginAPI === undefined) {
            _pluginAPI = new Common.API.Plugin();
        }
        _pluginAPI.setOffIdleEvent();
        _pluginAPI.setOnScreenSaver();
        
        if (_pluginAudio === null || _pluginAudio === undefined) {
            _pluginAudio = document.getElementById("pluginAudio");
        }
        _flushDataTimer = _createTimer(4000, this, _flushLocalData);
        _flushDataTimer.start();
        __stage = options.container;
        return __stage;
    }

    // Now initializes persistent storage
    function _initialize(options) {
        // perform any other setup here.
        __eventCallback.call(__eventReceiver, "INITIALIZE_COMPLETE", {});
    }
    
    function _clearLocalData() {
        
        _persistDict = {};
        _persistDictChanged = true;
        _flushLocalData();
    }
    
    function _writeLocalData(file, key, value) {
        if (!_persistDict[file]) {
            _persistDict[file] = {};
        }
        if (_persistDict[file][key] !== value) {
            _persistDict[file][key] = value;
            _persistDictChanged = true;
        }
    }
    
    function _readLocalData(file, key) {
        if (!_persistDict[file]) {
            _persistDict[file] = {};
            _persistDict[file][key] = "";
        }
        return _persistDict[file][key];
    }
    
    function _flushLocalData() {
        if (_persistDictChanged === true) {
            
            localDataFile = _fileSystem.openCommonFile(curWidget.id + "/huludata.dat", "w");
            if (localDataFile !== null) {
                
                describe(localDataFile);
                
                localDataFile.writeAll(_stringifyJSON(_persistDict));
                _fileSystem.closeCommonFile(localDataFile);
            }
            _persistDictChanged = false;
        }
        
    }
     
    // if you don't stop the player it freezes the TV.
    function _deinitialize(options) {
        // flush persistent storage
        _flushLocalData();
        if (_pluginPlayer) {
            _pluginPlayer.Stop();
        }
        _releasePlayer();
        
    }
    
    // loadURL
    //   input: url to read, callback function to call, a dictionary of options to pass to the handler
    //   
    //   launch an asynchronous url request, attaching options to the request object
    //
    function _loadURL(url, receiver, callback, options) {
        var req = new XMLHttpRequest();
        req.onreadystatechange = function() {
            _onLoadURLResult(req);
        };
        if (options.method != "POST") {
            options.method = "GET";
        }
        
        // Apply basic auth if username/password is in options
        if (options.auth_username && options.auth_password) {
            // NOTE: Adding basic auth params doesn't work in emulator (still get 403)
            req.open(options.method, url, true, options.auth_username, options.auth_password);
            
            // http://user:pw@www.domain.com doesn't work either (get 1002, weird)
            //url = url.replace("http://", "http://" + options.auth_username + ":" + options.auth_password + "@");
        }
        else {
            req.open(options.method, url, true);
        }
        
        options.receiver = receiver;
        options.callback = callback;
        options.reqUrl = url;
        req.options = options;
        
        if (options.content_type) {
           
           req.setRequestHeader("Content-type", options.content_type);
        }
        if (options.hasOwnProperty("compress") && options.compress) {
           req.setRequestHeader("X-Compress", "gzip");
        }
        
        
        
        if (options.timeout) {
            /*
            setTimeout(function() {
                req.abort();
                options.errorCallback.call(options.receiver, "request timeout");
            }, options.timeout);
            */
        }
        
        if (options.method != "POST" || !options.body || options.body.length === 0) {
            req.send();
        } else {
            req.setRequestHeader('Content-length', options.body.length);
            req.send(options.body);
        }
    }
    
   
    
    function _onLoadURLResult(req) {
        if (req.readyState == 4) {
        
            if (req.status != 200) {
                if (req.options.errorCallback) {
                    req.options.errorCallback.call(req.options.receiver, req.status, {
                        errorMessage: req.statusText,
                        data: req.responseText
                    });
                }
                else {
                    
                }
            }
            else {
                if (req.options.xmlparse) {
                    req.options.callback.call(req.options.receiver, req.responseXML, req.options);
                }
                else {
                    req.options.callback.call(req.options.receiver, req.responseText, req.options);
                }
            }
            req.options = null;
            req.destroy();
        }
        
    }
    
    function _initBufferingDaemon(url, progress) {
        _bufferingDaemon.timer.stop();
        if (_bufferingDaemonEnabled) {
            _bufferingDaemon.timer.setInterval(3000);
            _bufferingDaemon.timer.setCallback(this, function () { _retryVideoCallback(url, progress); });
            _bufferingDaemon.count = 1;
            _bufferingDaemon.monitoring = true;
        }
    }

    function _resetBufferingDaemon() {
        _bufferingDaemon.timer.stop();
        _bufferingDaemon.monitoring = false;
        _bufferingDaemon.count = 0;
    }

    function _retryVideoCallback(url, progress) {
        _bufferingDaemon.timer.stop();
        if (_bufferingDaemon.count > 0) {
            _stopVideo();
            var old_count = _bufferingDaemon.count;
            _playVideo(url, progress);
            _bufferingDaemon.count = old_count - 1;
            
        }
        else {
            
            _stopVideo();
            $htv.Platform.playerEvent(0, "onRenderingComplete", {});
        }
    }

    function _setWatchdogState(value) {
        if (_pluginAPI === null || _pluginAPI === undefined) {
            _pluginAPI = new Common.API.Plugin();
        }
        if (value == 0) {
            _pluginAPI.setOffScreenSaver();
        } else if (value == 1) {
            _pluginAPI.setOnScreenSaver();
        }
    }
    /*
    function _playerTimerFired(){
        LOG("TIMER FIRED" + videoPlayers['view-VideoPlayer'].player._currentVideoPos)
    }
    */
    function _initializePlayer(receiver, canvasCallback) {
        var success = false;
        _pluginPlayer = document.getElementById("pluginPlayer");
        _setWatchdogState(0);
        
        if (_pluginPlayer) {
            _pluginMW = document.getElementById("pluginObjectTVMW");
            
            if (_pluginMW) {
                // Save current TV Source
                _originalMWSource = _pluginMW.GetSource();
                
                // Set TV source to media player plugin  - 43 in the enum
                _pluginMW.SetSource(43);
                
                if (_pluginAudio) {
                    success = true;
                }
            }
        }
        
        if (success) {
            _prc = receiver;
            _pcc = canvasCallback;
            _playerLoaded = true;
           
            _resizePlayer(_currentPlayerPosition, 0);
            // Hook up event handlers
            _pluginPlayer.OnAuthenticationFailed = _onAuthenticationFailed;
            _pluginPlayer.OnBufferingComplete = _onBufferingComplete;
            _pluginPlayer.OnBufferingProgress = _onBufferingProgress;
            _pluginPlayer.OnBufferingStart = _onBufferingStart;
            _pluginPlayer.OnConnectionFailed = _onConnectionFailed;
            _pluginPlayer.OnCurrentPlayTime = _onCurrentPlayTime;
            _pluginPlayer.OnNetworkDisconnected = _onNetworkDisconnected;
            _pluginPlayer.OnRenderError = _onRenderError;
            _pluginPlayer.OnRenderingComplete = _onRenderingComplete;
            _pluginPlayer.OnStreamInfoReady = _onStreamInfoReady;
            _pluginPlayer.OnStreamNotFound = _onStreamNotFound;
        }
        else {
            _playerLoaded = false;
        }
        
        
    }
    
    function _releasePlayer() {
        _resizePlayer({x : 0, y : 0, height : 0, width : 0}, 0);
        _setWatchdogState(1);
        if (_pluginMW && _originalMWSource != null) {
            _updateDivScreens();
            // Restore original TV source before closing the widget
            _pluginMW.SetSource(_originalMWSource);
            
        }
    }
    
    function _updateDivScreens() {
        var topDiv, leftDiv, rightDiv, bottomDiv;
        topDiv = document.getElementById("top");
        bottomDiv = document.getElementById("bottom");
        leftDiv = document.getElementById("left");
        rightDiv = document.getElementById("right");
        
        topDiv.style.width = _currentPlayerPosition.x + _currentPlayerPosition.width;
        topDiv.style.height = _currentPlayerPosition.y;
        topDiv.style.top = 0;
        topDiv.style.left = 0;
          
        bottomDiv.style.width = 960 - _currentPlayerPosition.x;
        bottomDiv.style.height = 540 - _currentPlayerPosition.y - _currentPlayerPosition.height;
        bottomDiv.style.top = _currentPlayerPosition.y + _currentPlayerPosition.height;
        bottomDiv.style.left = _currentPlayerPosition.x;
        
        leftDiv.style.width = _currentPlayerPosition.x;
        leftDiv.style.height = 540 - _currentPlayerPosition.y;
        leftDiv.style.top = _currentPlayerPosition.y;
        leftDiv.style.left = 0;
        
        rightDiv.style.width = 960 - _currentPlayerPosition.x - _currentPlayerPosition.width;
        rightDiv.style.height = _currentPlayerPosition.y + _currentPlayerPosition.height;
        rightDiv.style.top = 0;
        rightDiv.style.left = _currentPlayerPosition.x + _currentPlayerPosition.width;
    }

    function _resizePlayer(position, duration) {
        // Copy raw properties since position object is modified
        _currentPlayerPosition = {
            x: position.x,
            y: position.y,
            width: position.width,
            height: position.height
        };
        
        if (position.height == 540) {
            // maximize
        }
        else if (position.height == 270) {
            // endcredits
            switch (_getModelNumber()) {
            case "BD-C6900":
                position.x -= 1;
                position.y += 3;
                break;
            default:
                // Use core-provided values
                break;
            }
        }
        else {
            // minimized
            switch (_getModelNumber()) {
            case "BD-C5500":
            case "BD-C6500":
                position.x -= 5;
                position.y -= 1;
                break;
            case "BD-C6900":
                position.x -= 24;
                position.y += 7;
                break;
            default:
                // Use core-provided values
                break;
            }
        }
        
        // resize div screens
        _updateDivScreens();
        
        _pluginPlayer.SetDisplayArea(position.x, position.y, position.width, position.height);
        
        if (_minimizeMaximizeHackEnabled === true &&  _playerPaused === true) {
            _pauseVideo();
        }
    }
    
    // use a playlist object instead.
    function _playVideo(url, startTime) {
        
        
        // Playing a video will cause a buffering event, so we'll simply ignore it in our count.
        resetBufferingEventStats();
        ignoreFollowingBufferingEvent();
        
        if ((url.indexOf("https") !== 0) && (_bufferingDaemonEnabled === true)) {
            _initBufferingDaemon(url, startTime);
        } else {
            _resetBufferingDaemon();
        }         

        // Reset player time (updated by OnCurrentPlayTime) and set pending seek time
        _playerPaused = false;
        
        _lastPlayerTime = 0;
        _pendingSeekTime = startTime;
        _firstProgressEvent = true;
        
        try {
            // Stop first to prevent emulator crash
            _pluginPlayer.Stop();
            
            _resizePlayer(_currentPlayerPosition, 0);
                
            if (startTime > 0) {
                // NOTE: ResumePlay == Play, at least in emulator
                _pluginPlayer.ResumePlay(url, startTime * 0.001 /* sec */);
            }
            else {
                _pluginPlayer.Play(url);
            }
        } 
        catch (e) {
            
        }
    }
    
    function resetBufferingEventStats() {
        _bufferingEvents = [];
        _cumulativeTimeSinceLastBufferingEvent = 0;
        _ignoreNextBufferingEvent = false;
    }
    
    function ignoreFollowingBufferingEvent() {
        _ignoreNextBufferingEvent = true;
    }
    
    function registerStartOfBufferingEvent() {
        if (_ignoreNextBufferingEvent) {
            _ignoreNextBufferingEvent = false;
            return;
        }
        
        _bufferingEvents.push(_cumulativeTimeSinceLastBufferingEvent);
        _cumulativeTimeSinceLastBufferingEvent = 0;
    }
    
    function pruneOldBufferingEvents() {
        var sum = 0;
        
        var lastLiveIndex;
        for (lastLiveIndex = _bufferingEvents.length - 1; lastLiveIndex >= 0; lastLiveIndex--) {
            sum += _bufferingEvents[lastLiveIndex];
            if (sum > _bufferingWindowPeriod) {
                break;
            }
        }
        
        var numberOfEventsToPrune = lastLiveIndex;
        if (numberOfEventsToPrune > 0) {
            _bufferingEvents.splice(0, numberOfEventsToPrune);
        }
    }
    
    function _hasExceededBufferingEventThreshold() {
        pruneOldBufferingEvents();
        
        var sum = 0;
        for (var i = _bufferingEvents.length - 1; i >= 0 && i > _bufferingEvents.length - _bufferingThreshold; i--) {
            sum += _bufferingEvents[i];
        }
        
        if (_bufferingEvents.length >= _bufferingThreshold) { // There have been enough buffering events to raise the flag.
            if (sum <= _bufferingWindowPeriod) {
                return true;
            }
        }
        return false;
    }
    
    function _playerEvent(playerId, identifier, event) {
        
        var eventType = "UNHANDLED";
        var eventData = {};
        switch (identifier) {
            case "onCurrentPlayTime":
                // HACK: Seeking right after receiving the buffer complete
                // event doesn't work, but waiting a bit does
                if (_resumePlayHackStageThreeEnabled && _pendingSeekTime > 0) {
                    _seekTo(0, _pendingSeekTime);
                    _pendingSeekTime = 0;
                    return;     // Don't fire progress event
                }
                
                if (_isSeeking) {
                    _isSeeking = false;
                }
                else {
                    _cumulativeTimeSinceLastBufferingEvent += event.milliseconds - _lastPlayerTime;
                }

                eventType = "TIME_PROGRESS";
                eventData.milliseconds = event.milliseconds;
                _lastPlayerTime = event.milliseconds;
                break;
            case "onBufferingComplete":
                // HACK: Seeking right after receiving the buffer complete
                // event doesn't work, but waiting a bit does
                if (_resumePlayHackStageTwoEnabled && _pendingSeekTime > 0) {
                    setTimeout(function(time) {
                        _seekTo(0, time);
                    }, 200, _pendingSeekTime);
                    _pendingSeekTime = 0;
                    return;     // Don't fire progress event
                }
                
                _bufferingDaemon.timer.stop();
                eventType = "STATE_CHANGE";
                eventData.state = "BufferingComplete";
                break;
            case "onBufferingProgress":
                eventType = "STATE_CHANGE";
                eventData.state = "BufferingProgress";
                _resetBufferingDaemon();
                break;
            case "onBufferingStart":
                registerStartOfBufferingEvent();
                
                if (_bufferingDaemonEnabled === true && _bufferingDaemon.monitoring === true) { 
                    _bufferingDaemon.timer.start();
                }
                else {
                    _resetBufferingDaemon();
                }

                eventType = "STATE_CHANGE";
                eventData.state = "BufferingStarted";
                eventData.excessiveBuffering = _hasExceededBufferingEventThreshold();
                break;
            case "onRenderingComplete":
                _stopVideo();
                eventType = "PLAYBACK_ENDED";
                break;
            case "onStreamInfoReady":
                // HACK: Seeking right after receiving the buffer complete
                // event doesn't work, but waiting a bit does
                if (_resumePlayHackStageOneEnabled && _pendingSeekTime > 0) {
                    _seekTo(0, _pendingSeekTime);
                    _pendingSeekTime = 0;
                    return;     // Don't fire progress event
                }
                
                eventType = "STREAM_INFO_READY";
                eventData = {
                    duration: _pluginPlayer.GetDuration()
                }
                break;
            case "onRenderError":
                eventType = "RENDERING_ERROR";
                eventData = { message: event.message };
                break;
            case "onStreamNotFound":
                eventType = "STREAM_NOT_FOUND";
                eventData = {};
                break;
            case "onConnectionFailed":
                eventType = "CONNECTION_FAILED";
                break;
            case "onNetworkDisconnected":
                eventType = "NETWORK_DISCONNECTED";
                break;
            default:
                eventData = event.payload;
                
                break;
        }
        // bubble up
        __playerCallback.call(__playerReceiver, playerId, eventType, eventData);
    }
    
    function _nextVideo(playerId) {
        
    }
    
    function _pauseVideo(){
        _playerPaused = true;
        if (_pluginPlayer) {
            _pluginPlayer.Pause();
        }
    }
    
    function _resumeVideo(){
        _playerPaused = false;
        if (_pluginPlayer) {
            _pluginPlayer.Resume();
        }
    }
    
    // todo: move a little earlier?  so we don't lose half seconds 
    function _seekBy(playerId, offset /* ms */) {
        if (offset > 0) {
            _pluginPlayer.JumpForward(offset * 0.001 /* sec */);
        }
        else if (offset < 0) {
            _pluginPlayer.JumpBackward(Math.abs(offset * 0.001 /* sec */));
        }    
    }
    
    function _seekTo(playerId, time /* ms */) {
        
        _isSeeking = true;
        ignoreFollowingBufferingEvent();
        _seekBy(playerId, time - _lastPlayerTime);
        _resumeVideo();
    }

    function _stopVideo(){
        _playerPaused = false;
        _pluginPlayer.Stop();
        _resetBufferingDaemon();
    }
    
    function _switchBitrate() {
        
    }
    
    function _increaseVolume() {
        if (_isMuted() === true) {
            _toggleMuteState();
        }
        _pluginAudio.SetVolumeWithKey(0);
    }
    
    function _decreaseVolume() {
        if (_isMuted() === true) {
            _toggleMuteState();
        }
        _pluginAudio.SetVolumeWithKey(1);
    }
    
    // deprecated in newest API, probably should rewrite to call increase/decrease
    // so that the player can maintain a constant volume level across videos..?
    function _setVolume(newVolume) {
        _pluginAudio.SetRelativeVolume(newVolume - _getVolume());
        
    }

    function _getVolume() {
        return _pluginAudio.GetVolume() / 100;
    }
    
    function _isMuted() {
        if (_userMuteState === null) {
            try {
                _userMuteState = _pluginAudio.GetUserMute();
            }
            catch (e) {
                _userMuteState = 1;
            }
        }
        
        // If _userMuteState < 0, thus indicating an error, we return true.
        return _userMuteState === 1 || _userMuteState < 0;
    }
    
    function _toggleMuteState() {
        // If _userMuteState < 0, thus indicating an error, we do nothing; else, we flip state.
        if (_isMuted() === true) {
            if (_userMuteState > 0) {
                _userMuteState = 0;
                try {
                    _pluginAudio.SetUserMute(_userMuteState);
                } catch (e) { }
            }
        }
        else {
            _userMuteState = 1;
            try {
                _pluginAudio.SetUserMute(_userMuteState);
            } catch (e) { }
        }
    }

    function _playerRegister(playerId, playerHandle, containerHandle){
        
        _pcc.call(_prc, containerHandle);
    }
    
    function _getDeviceFriendlyName() {
        var ret = "Samsung Other";
        switch(_getPlatform()) {
            case "TV":
                ret = "Samsung TV";
                break;
            case "BD":
                ret = "Samsung BluRay";
                break;
            default:
                break;
        }
        return ret + " (" + _getModelNumber() + ")";
    }
    
    function _getDeviceGUID() {
        if (!_pluginNetwork) {
            _pluginNetwork = document.getElementById("pluginNetwork");
        }
        if (!_pluginNNavi) {
            _pluginNNavi = document.getElementById("pluginObjectNNavi");
        }
        
        var duid = "sssssssssssss";
        try {
            var mac = _pluginNetwork.GetHWaddr();
            duid = _pluginNNavi.GetDUID(mac);
        }
        catch(e) {
            
        }
        return duid;
    }
    
    function _isEmulator() {
        return _getDeviceGUID() == "sssssssssssss";
    }

    function _getBandwidth() {
        return _currentBandwidth;
    }
    
    function _getDefaultBitrate() {
        return _defaultBitrate;
    }
    
    function _runSpeedTest() {
        var req = new XMLHttpRequest();
        var startTime, endTime;
        req.open("GET", "http://ingest.hulu.com/babel/speedtest.flv", false);
        startTime = new Date().getTime();  
        req.send();
        endTime = new Date().getTime();  
        
        _currentBandwidth = (339097/(endTime - startTime));
        
    }
    
    var _retrievedPlatform = null;
    function _getPlatform() {
        if (_retrievedPlatform !== null) {
            return _retrievedPlatform;
        }
        
        if (!_pluginTV) {
            _pluginTV = document.getElementById("pluginTV");
        }
        var platform = "TV";
        try {
            // 0:TV 1: MONITOR, 3: BD
            platform = (_pluginTV.GetProductType() == 0) ? "TV" : "BD";
            _retrievedPlatform = platform;
        }
        catch(e) {
            
        }
        return platform;
    }
    
    var _retrievedModelNumber = null;
    function _getModelNumber() {
        if (_retrievedModelNumber !== null) {
            return _retrievedModelNumber;
        }

        if (!_pluginTV) {
            _pluginTV = document.getElementById("pluginTV");
        }
        var product_code = "SAMSUNG_DEVICE";
        try {
            if (!_isEmulator()) { 
                product_code = _pluginTV.GetProductCode(1);
                _retrievedModelNumber = product_code;
            }
        }
        catch(e) {
            
        }

        return product_code;
    }
    
    function _decryptAES(cipherText, key, iv, gzipInflate) {
        var cipher_bytes = $htv.Libs.Crypto.util.hexToBytes(cipherText);
        var initialization_vector = $htv.Libs.Crypto.util.hexToBytes(iv);
        var encrypted_bytes = initialization_vector.concat(cipher_bytes);
        var key_bytes = $htv.Libs.Crypto.util.hexToBytes(key);
        return $htv.Libs.Crypto.AES.decrypt(encrypted_bytes, key_bytes, {
            mode: $htv.Libs.Crypto.mode.CBC7,
            use_pkcs7: true
        });
    }
    
    function _postInitialize(platformstatus, model_default_bitrates, version_info) {
        // store platformstatus from deejay
        // check model number against platform status to enable/disable hacks and such.
        var modelNumber = _getModelNumber();
        var capabilitiesValue = 4; // "000100" : only minimize/maximize hack enabled
        var disabledMessage = "We're sorry, the Hulu Plus application is not yet compatible with this device. Please see http://www.hulu.com/plus/devices for a full list of supported devices.";
        
        if (platformstatus) {
            if (platformstatus[modelNumber]) {
                
                capabilitiesValue = platformstatus[modelNumber];
            } else if (platformstatus["default"]) {
                
                capabilitiesValue = platformstatus["default"];
            }
            if (platformstatus["disabled_message"] && platformstatus["disabled_message"].length > 0) {
                disabledMessage = platformstatus["disabled_message"];
            }
            if (platformstatus["buffering_window_period"]) {
                _bufferingWindowPeriod = platformstatus.buffering_window_period;
            }
            if (platformstatus["buffering_threshold"]) {
                _bufferingThreshold = platformstatus.buffering_threshold;
            }
        } else {
            
        }
        
        if (model_default_bitrates) {
            if (model_default_bitrates[modelNumber]) {
                _defaultBitrate = model_default_bitrates[modelNumber];
            } else if (model_default_bitrates["default"]) {
                _defaultBitrate = model_default_bitrates["default"];
            }
        }
        
        var platformDisabled = (capabilitiesValue & 1) > 0;
        if (platformDisabled === true) {
            
            __eventCallback.call(__eventReceiver, "PLATFORM_STATUS_ERROR", {error_id: "PLATFORM_NOT_SUPPORTED", error_text: disabledMessage});
        } else {
            _bufferingDaemonEnabled = (capabilitiesValue & 2) > 0;
            if (_bufferingDaemonEnabled === true) {
                
            }
            _minimizeMaximizeHackEnabled = (capabilitiesValue & 4) > 0;
            if (_minimizeMaximizeHackEnabled === true) {
                
            }
            _resumePlayHackStageOneEnabled = (capabilitiesValue & 8) > 0;
            if (_resumePlayHackStageOneEnabled === true) {
                
            }
            _resumePlayHackStageTwoEnabled = (capabilitiesValue & 16) > 0;
            if (_resumePlayHackStageTwoEnabled === true) {
                
            }
            _resumePlayHackStageThreeEnabled = (capabilitiesValue & 32) > 0;
            if (_resumePlayHackStageThreeEnabled === true) {
                
            }
        }
    }
    
    function _recordEvent(eventName, eventData) {
        
        if (eventData) {
            describe(eventData);
        }
    }
    
    
    
    return {
        // public interface
        attachEventHandlers: _attachEventHandlers,
        bootstrap: _bootstrap,
        createContainer: _createContainer,
        createImage: _createImage,
        createLabel: _createLabel,
        createBox: _createBox,
        createSpacer: _createSpacer,
        createTextArea: _createTextArea,
        createTimer: _createTimer,
        decreaseVolume: _decreaseVolume,
        decryptAES : _decryptAES,
        deinitialize: _deinitialize,
        deleteItem: _deleteItem,        
        runSpeedTest: _runSpeedTest,
        getBandwidth: _getBandwidth,
        evaluateXPath: _evaluateXPath,
        evaluateXPathInt: _evaluateXPathInt,
        evaluateXPathNumber: _evaluateXPathNumber,
        evaluateXPathString: _evaluateXPathString,
        evaluateXPathAttributeString: _evaluateXPathAttributeString,
        exit: _exit,
        parseJSON: _parseJSON,
        clearLocalData: _clearLocalData, 
        readLocalData: _readLocalData,
        writeLocalData: _writeLocalData,
        properties: {
            client: _getPlatform(),
            deviceid: _getDeviceGUID(),
            distro: "Samsung",
            distroplatform: _getPlatform() === "BD" ? "BluRay" : "TV",
            friendly_name: _getDeviceFriendlyName(),
            visible_friendly_name: _getDeviceFriendlyName(),
            model: _getModelNumber(),
            visible_model: _getModelNumber(),
            has_spotlight: false,
            os: "Samsung Apps", // TODO: get platform version number
            platformCode: _getPlatform() === "BD" ? 6 : 2,
            platformVersion: _getPlatform() === "BD" ? 2 : 2,
            platformKey: _getPlatform() === "BD" ? "69317c5c6d6e202a493d3b6c28522843" : "4f722a3d49272d3e243321413f7d6a3c",
            site_sec: "hv2EbTCRK7SZOnAIXgMmxUzTXMR",
            site_app: "1ae39f67966543e805c371e61428d29b736a660c",
            marketing_version: "2.01",
            vSize: 540,
            livestreaming: false,
            livestreaming_autoswitch: false,
            player: 0.3, // TODO: app version number
            press_and_hold: !_isEmulator(),
            user: {},
            has_browse_while_playing: true,
            has_stop_button: true,
            has_quality_button: false,
            has_unicode: true,
            has_reflections: false,
            has_animations: false,
            has_in_app_exit: true,
            has_gzip: true,
            has_gzip_decrypt: false
        },
        // from platform specific players
        getDefaultBitrate: _getDefaultBitrate,
        getDeviceGUID: _getDeviceGUID,
        getVolume: _getVolume,
        getXMLChildAt: _getXMLChildAt,
        getXMLChildrenCount: _getXMLChildrenCount,
        getXMLNodeName: _getXMLNodeName,
        getXMLNodeText: _getXMLNodeText,
        getXMLRootNode: _getXMLRootNode,
        getXPathResultAt: _getXPathResultAt,
        enableSpotlight: function() {},
        disableSpotlight: function() {},
        hideSpotlight: function(overrideNextAnimation) {},
        updateSpotlight: function() {},
        increaseVolume: _increaseVolume,
        initialize: _initialize,
        postInitialize: _postInitialize,
        initializePlayer: _initializePlayer,
        isMuted: _isMuted,
        keyDownHandler: _keyDownHandler, // Called by index.html
        keyUpHandler: _keyUpHandler, // Called by index.html
        loadURL: _loadURL,
        nextVideo: _nextVideo,
        pauseVideo: _pauseVideo,
        playerEvent: _playerEvent,
        playerRegister: _playerRegister,
        playVideo: _playVideo,
        releasePlayer: _releasePlayer,
        resizePlayer: _resizePlayer,
        resumeVideo: _resumeVideo,
        seekBy: _seekBy,
        seekTo: _seekTo,
        stopVideo: _stopVideo,
        stringToXMLDoc: _stringToXMLDoc,
        switchBitrate: _switchBitrate,
        toggleMuteState: _toggleMuteState,
        recordEvent: _recordEvent
    }
}();


describe($htv.Platform.properties);
// Player event handlers
function _onAuthenticationFailed() {
    $htv.Platform.playerEvent(0, "onAuthenticationFailed", {});
};

function _onBufferingComplete() {
    $htv.Platform.playerEvent(0, "onBufferingComplete", {});
};

function _onBufferingProgress(percent) {
    $htv.Platform.playerEvent(0, "onBufferingProgress", {});
};

function _onBufferingStart() {
    $htv.Platform.playerEvent(0, "onBufferingStart", {});
};

function _onConnectionFailed() {
    $htv.Platform.playerEvent(0, "onConnectionFailed", {});
};

function _onCurrentPlayTime(time /* ms */) {
    $htv.Platform.playerEvent(0, "onCurrentPlayTime", { milliseconds: parseInt(time) });
};

function _onNetworkDisconnected() {
    $htv.Platform.playerEvent(0, "onNetworkDisconnected", {});
};

/* 
 * 1 : Unsupported container
 * 2 : Unsupported video codec 
 * 3 : Unsupported audio codec
 * 4 : Unsupported video resolution
 */
 
var renderErrorTypeMessages = {
    1 : "Unsupported container",
    2 : "Unsupported video codec",
    3 : "Unsupported audio codec",
    4 : "Unsupported video resolution"
}
 
function _onRenderError(renderErrorType) {
    $htv.Platform.playerEvent(0, "onRenderError", {message: renderErrorTypeMessages[renderErrorType]});
};

function _onRenderingComplete() {
    $htv.Platform.playerEvent(0, "onRenderingComplete", {});
};

function _onStreamInfoReady() {
    $htv.Platform.playerEvent(0, "onStreamInfoReady", {});
};

function _onStreamNotFound() {
    $htv.Platform.playerEvent(0, "onStreamNotFound", {});
};
