/*
 * ShowController.js
 * Keynote HTML Player
 * 
 * Responsibility: Tungwei Cheng
 * Copyright (c) 2009-2016 Apple Inc. All rights reserved.
 */

var kShowControllerState_Stopped = "Stopped";
var kShowControllerState_Starting = "Starting";
var kShowControllerState_DownloadingScript = "DownloadingScipt";
var kShowControllerState_SettingUpScene = "SettingUpScene";
var kShowControllerState_IdleAtFinalState = "IdleAtFinalState";
var kShowControllerState_IdleAtInitialState = "IdleAtInitialState";
var kShowControllerState_WaitingToJump = "WaitingToJump";
var kShowControllerState_ReadyToJump = "ReadyToJump";
var kShowControllerState_WaitingToDisplay = "WaitingToDisplay";
var kShowControllerState_ReadyToDisplay = "ReadyToDisplay";
var kShowControllerState_WaitingToPlay = "WaitingToPlay";
var kShowControllerState_ReadyToPlay = "ReadyToPlay";
var kShowControllerState_Playing = "Playing";

// Events:
// -------
var kKeyDownEvent = "keydown";
var kSlideIndexDidChangeEvent = "ShowController:SlideIndexDidChangeEvent";

var ShowController = Class.create({
    initialize: function() {
        // extract delegate from url or create a default delegate
        this.delegate = extractDelegateFromUrlParameter();
        this.delegate.showDidLoad();

        this.showUrl = "../";

        // These must be created before the OrientationController as they
        // subscribe to its event
        this.displayManager = new DisplayManager();
        this.scriptManager = new ScriptManager(this.showUrl);
        this.textureManager = new TextureManager(this.showUrl);
        this.stageManager = new StageManager(this.textureManager, this.scriptManager);
        this.touchController = new TouchController();
        this.animationManager = new AnimationManager();
        this.orientationController = new OrientationController();

        this.activeHyperlinks = new Array();
        this.movieHyperlinks = new Array();

        // initialize default values
        this.script = null;
        this.currentSceneIndex = -1;
        this.nextSceneIndex = -1;
        this.currentSlideIndex = -1;
        this.previousSlideIndex = -1;
        this.currentSoundTrackIndex = 0;
        this.transformOriginValue = "";
        this.accumulatingDigits = false;
        this.digitAccumulator = 0;
        this.firstSlide = true;
        this.lastSlideViewedIndex = -1;

        this.accountID = "";
        this.guid = "";
        this.locale = "EN";
        this.isNavigationBarVisible = false;
        this.isFullscreen = false;
        this.volume = 3.0;
        this.muted = false;

        this.soundTrackPlayer = null;
        this.sceneIndexOfPrebuiltAnimations = -1;

        // store queued user action
        this.queuedUserAction = null;

        // events
        document.observe(kScriptDidDownloadEvent, this.handleScriptDidDownloadEvent.bind(this));
        document.observe(kScriptDidNotDownloadEvent, this.handleScriptDidNotDownloadEvent.bind(this));
        document.observe(kStageIsReadyEvent, this.handleStageIsReadyEvent.bind(this));
        document.observe(kStageSizeDidChangeEvent, this.handleStageSizeDidChangeEvent.bind(this));

        // swipe and keydown events
        document.observe(kKeyDownEvent, this.handleKeyDownEvent.bind(this));
        document.observe(kSwipeEvent, this.handleSwipeEvent.bind(this));

        // mouse event
        Event.observe(this.displayManager.body, "click", this.handleClickEvent.bind(this));

        // fullscreen change event
        document.observe(kFullscreenChangeEventName, this.handleFullscreenChangeEvent.bind(this));

        // windows resize event
        Event.observe(window, "resize", this.handleWindowResizeEvent.bind(this));

        // Can't use event observer for tap events
        // - this would cause the handler to be on a seperate event loop
        // invocation
        // - this would prevent us from doing this like opening new tabs (popup
        // blocker logic kicks in)
        this.touchController.registerTapEventCallback(this.handleTapEvent.bind(this));

        // Initialize state to Stopped
        this.changeState(kShowControllerState_Stopped);

        // movie cache to be used across different event timeline within one slide
        this.movieCache = null;

        // audio cache
        this.audioCache = null;

        // KPF playback controller
        this.playbackController = new KPFPlaybackController({}, this.stageManager.stage);

        // Navigator
        this.navigatorController = new NavigatorController(document.getElementById("slideshowNavigator"));

        // Slide number feedback
        this.slideNumberController = new SlideNumberController(document.getElementById("slideNumberControl"));

        // Slide number display
        this.slideNumberDisplay = new SlideNumberDisplay(document.getElementById("slideNumberDisplay"));

        // Help Placard
        this.helpPlacard = new HelpPlacardController(document.getElementById("helpPlacard"));

        // indicate if the show has recording
        this.isRecording = false;

        // a boolean to indicate if the recording is started
        this.isRecordingStarted = false;

        // IE9 does not support CSS animations
        if (isIE && browserVersion < 10) {
            this.animationSupported = false;
        }
        else {
            this.animationSupported = true;
        }

        // disable mouse right click context menu
        document.observe("contextmenu", this.handleContextMenuEvent.bind(this));

        Event.observe(this.displayManager.previousButton, "click", this.goBackToPreviousSlide.bind(this, "tapPreviousButton"));
        Event.observe(this.displayManager.nextButton, "click", this.advanceToNextBuild.bind(this, "tapNextButton"));
    },

    startShow: function() {
        this.changeState(kShowControllerState_DownloadingScript);
        this.scriptManager.downloadScript(this.delegate);
    },

    exitShow: function(endNow) {
        clearTimeout(this.exitTimeout);

        if (endNow) {
            this.delegate.showExited();
        } else {
            this.exitTimeout = setTimeout((function(){
                this.delegate.showExited();
            }).bind(this), 750);
        }
    },

    promptUserToTryAgain: function(message) {
        var tryAgain = false;

        tryAgain = confirm(message);
        return tryAgain;
    },

    handleScriptDidDownloadEvent: function(event) {
        switch (this.state) {
        case kShowControllerState_DownloadingScript:
            var script = this.script = event.memo.script;
            var showMode = script.showMode;

            if (showMode == kShowModeHyperlinksOnly) {
                this.displayManager.setHyperlinksOnlyMode();
            }

            this.changeState(kShowControllerState_Starting);

            // checking to see if a restarting scene index was specified in url...
            var sceneIndex;
            var restartingSceneIndex = parseInt(getUrlParameter("restartingSceneIndex"));

            // also look for a fragment identifier which can also indicate scene index
            var currentUrl = document.URL.split("?");
            var fragments = currentUrl[0].split("#");
            if (fragments[1]) {
                restartingSceneIndex = parseInt(fragments[1]);
            }

            if (restartingSceneIndex) {
                // found a restarting scene index, using that...restartingSceneIndex
                sceneIndex = restartingSceneIndex;
            } else {
                // checking to see if a starting slide number was specified in url...
                var startingSlide = getUrlParameter("currentSlide");
                var startingSlideNumber;

                if (startingSlide) {
                    startingSlideNumber = parseInt(startingSlide);
                } else {
                    // nope, not there, use 1...
                    startingSlideNumber = 1;
                }
                sceneIndex = this.scriptManager.sceneIndexFromSlideIndex(startingSlideNumber - 1);
            }

            // if this show has recording, then we start the show in recording mode
            if (script.recording) {
                if (script.recording.eventTracks[0].type === "navigation") {
                    this.narrationManager = new NarrationManager(script.recording);
                    sceneIndex = this.narrationManager.sceneIndexFromNavigationEvent(this.narrationManager.navigationEvents[0]);
                    this.isRecording = true;
                    this.jumpToScene(sceneIndex, false);

                    break;
                }
            }

            if (sceneIndex > script.lastSceneIndex) {
                break;
            }

            if (showMode === kShowModeAutoplay) {
                this.jumpToScene(sceneIndex, true);
            } else {
                var event = script.events[sceneIndex];
                var automaticPlay = event.automaticPlay == 1 || event.automaticPlay == true;
                this.jumpToScene(sceneIndex, automaticPlay);
            }
            break;

        default:
            debugMessage(kDebugShowController_HandleScriptDidDownloadEvent,
                    "- hmmm we seem to have arrived here from an unpredicted state");
            break;
        }
    },

    handleScriptDidNotDownloadEvent: function(event) {
        debugMessage(kDebugShowController_HandleScriptDidNotDownloadEvent);

        var tryAgain = this.promptUserToTryAgain(kUnableToReachiWorkTryAgain);

        if (tryAgain) {
            this.scriptManager.downloadScript();
        } else {
            this.displayManager.clearLaunchMode();
            this.displayManager.hideWaitingIndicator();
        }
    },

    handleStageIsReadyEvent: function(event) {
        if (this.isFullscreen) {
            setTimeout((function() {
                this.displayManager.stageArea.style.opacity = 1;
            }).bind(this), 50)
        } else {
            setTimeout((function() {
                this.displayManager.stageArea.style.opacity = 1;
            }).bind(this), 500)
        }
        
        this.positionSlideNumberControl();
        this.positionSlideNumberDisplay();
        this.positionHelpPlacard();
    },

    positionSlideNumberControl: function() {
        var left = (this.displayManager.usableDisplayWidth - this.slideNumberController.width) / 2;
        var top = this.displayManager.stageAreaTop + this.displayManager.stageAreaHeight - (this.slideNumberController.height + 16);
        
        this.slideNumberController.setPosition(left, top);
    },

    positionSlideNumberDisplay: function() {
        var left = (this.displayManager.usableDisplayWidth - this.slideNumberDisplay.width) / 2;
        var top = this.displayManager.stageAreaTop + this.displayManager.stageAreaHeight - (this.slideNumberDisplay.height + 16);
        
        this.slideNumberDisplay.setPosition(left, top);
    },

    positionHelpPlacard: function() {
        var left = (this.displayManager.usableDisplayWidth - this.helpPlacard.width) / 2;
        var top = (this.displayManager.usableDisplayHeight - this.helpPlacard.height) / 2;
        
        this.helpPlacard.setPosition(left, top);
    },

    handleFullscreenChangeEvent: function() {
        if (document.webkitIsFullScreen || document.mozFullScreen) {
            this.isFullscreen = true;
        } else {
            this.isFullscreen = false;
        }

        setTimeout((function() {
            this.displayManager.layoutDisplay();
        }).bind(this), 0);
    },

    handleWindowResizeEvent: function() {
        clearTimeout(this.resizeTimer);
        this.resizeTimer = setTimeout(this.changeWindowSize.bind(this), 1000);
    },

    changeWindowSize: function() {
        if (this.delegate.setViewScale) {
            this.scriptManager.reapplyScaleFactor();

            this.textureManager.slideCache = null;
            this.textureManager.slideCache = {};

            var sceneIndexToUse = this.currentSceneIndex;

            if (this.state === kShowControllerState_IdleAtFinalState) {
                if (this.currentSceneIndex < this.script.numScenes - 1) {
                    sceneIndexToUse = this.currentSceneIndex + 1;
                } else {
                    if (this.script.loopSlideshow) {
                        sceneIndexToUse = 0;
                    }
                }
            }

            this.jumpToScene(sceneIndexToUse, false);
        }

        document.fire(kShowSizeDidChangeEvent, {
            width: this.script.slideWidth,
            height: this.script.slideHeight
        });
    },

    handleStageSizeDidChangeEvent: function(event) {
        // update TouchController with new track area
        this.touchController.setTrackArea(event.memo.left, event.memo.top, event.memo.width, event.memo.height);
    },

    handleKeyDownEvent: function(event) {
        var key = event.charCode || event.keyCode;

        // allow F11 and F12 to work on IE
        if (key === kKeyCode_F11 || key === kKeyCode_F12) {
            return;
        }

        var modifiers = {
            altKey: !!event.altKey,
            ctrlKey: !!event.ctrlKey,
            shiftKey: !!event.shiftKey,
            metaKey: !!event.metaKey
        };

        if (modifiers.metaKey) {
            if (key === kKeyCode_Period || key === kKeyCode_Dot) {
                // cmd - . to exit show
                this.exitShow(true);
            } else if (key != kKeyCode_Return) {
                // allow browsers to handle cmd-key
                return;
            }
        } else if (modifiers.ctrlKey) {
            // allow browsers to handle ctrl-key
            return;
        } 

        event.stop();
        this.onKeyPress(key, modifiers);
    },

    handleContextMenuEvent: function(event) {
        event.stop();
    },

    handleClickEvent: function(event) {
        if (this.isRecording) {
            return;
        }

        var x, y;

        if (event.pageX || event.pageY) {
            x = event.pageX;
            y = event.pageY;
        } else {
            x = event.clientX;
            y = event.clientY;
        }

        var displayCoOrds = {
            pointX: x,
            pointY: y
        };

        // for IE make sure windows has focus
        if (isIE) {
            window.focus();
        }

        // For video element, let the event to propagate
        if (event.target.nodeName.toLowerCase() === "video") {
            return;
        }

        this.processClickOrTapAtDisplayCoOrds(displayCoOrds);
    },

    handleTapEvent: function(event) {
        var displayCoOrds = {
            pointX: event.memo.pointX,
            pointY: event.memo.pointY
        };

        this.processClickOrTapAtDisplayCoOrds(displayCoOrds);
    },

    processClickOrTapAtDisplayCoOrds: function(displayCoOrds) {
        var isHyperlink = false;
        var hyperlink;

        if (this.slideNumberController.isShowing) {
            if (this.slideNumberTimeout) {
                clearTimeout(this.slideNumberTimeout);
            }
            this.slideNumberTimeout = setTimeout(this.hideAndResetSlideNumberController.bind(this), 0);
            return;
        }
        
        if (this.helpPlacard.isShowing) {
            this.helpPlacard.hide();
            return;
        }

        var showCoOrds = this.displayManager.convertDisplayCoOrdsToShowCoOrds(displayCoOrds);

        if (showCoOrds.pointX != -1) {
            hyperlink = this.findHyperlinkAtCoOrds(showCoOrds);
        }

        if (hyperlink) {
            this.processHyperlink(hyperlink);
        } else {
            this.advanceToNextBuild("processClickOrTapAtDisplayCoOrds");
        }
    },

    handleSwipeEvent: function(event) {
        if (event.memo.direction === "left") {
            switch (event.memo.fingers) {
            case 1:
                this.advanceToNextBuild("handleSwipeEvent");
                break;
            case 2:
                this.advanceToNextSlide("handleSwipeEvent");
                break;
            default:
                break;
            }
        } else if (event.memo.direction === "right") {
            switch (event.memo.fingers) {
            case 1:
                this.goBackToPreviousSlide("handleSwipeEvent");
                break;
            case 2:
                this.goBackToPreviousBuild("handleSwipeEvent");
                break;
            default:
                break;
            }
        }
    },

    onMouseDown: function(mouseDownEvent) {
        if (mouseDownEvent.leftClick) {
            this.advanceToNextBuild("onMouseDown");
        } else if (mouseDownEvent.rightClick) {
            this.goBackToPreviousBuild("onMouseDown");
        }
    },

    onKeyPress: function(key, modifier) {
        if ((key >= kKeyCode_Numeric_0) && (key <= kKeyCode_Numeric_9)) {
            key = kKeyCode_0 + (key - kKeyCode_Numeric_0);
        }

        key += (modifier.shiftKey ? kKeyModifier_Shift : 0);
        key += (modifier.altKey ? kKeyModifier_Alt : 0);
        key += (modifier.ctrlKey ? kKeyModifier_Ctrl : 0);
        key += (modifier.metaKey ? kKeyModifier_Meta : 0);

        if (this.isRecording) {
            return;
        }

        var digitEncountered = false;
        switch (key) {
        case kKeyCode_Escape:
            this.exitShow(true);
            break;

        /*
         * case kKeyCode_Return + kKeyModifier_Ctrl:
         * this.displayManager.showWaitingIndicator(); break;
         * 
         * case kKeyCode_Return + kKeyModifier_Alt:
         * this.displayManager.hideWaitingIndicator(); break;
         * 
         * case kKeyCode_Return + kKeyModifier_Meta: this.debugDiagnosticDump();
         * break;
         */
        case kKeyCode_Slash:
        case kKeyCode_Slash + kKeyModifier_Shift:
            if (this.helpPlacard.isShowing) {
                this.helpPlacard.hide();
            } else {
                this.helpPlacard.show();
            }
            break;

        case kKeyCode_Q:
            this.exitShow(true);
            break;

        case kKeyCode_S:
            if (this.slideNumberController.isShowing) {
                if (this.slideNumberTimeout) {
                    clearTimeout(this.slideNumberTimeout);
                }
                this.slideNumberTimeout = setTimeout(this.hideAndResetSlideNumberController.bind(this), 0);
            }
            
            if (this.slideNumberDisplay.isShowing) {
                this.slideNumberDisplay.hide();
            } else {
                this.slideNumberDisplay.setSlideNumber(this.currentSlideIndex + 1);
                this.slideNumberDisplay.show();
            }
            break;

        case kKeyCode_Return:
            if (this.accumulatingDigits) {
                // return pressed while accumulating digits.
                this.accumulatingDigits = false;

                if (this.script.showMode != kShowModeHyperlinksOnly) {
                    if (this.digitAccumulator > this.script.slideCount) {
                        this.digitAccumulator = this.script.slideCount;
                    }
                    else if (this.digitAccumulator < 1) {
                        this.digitAccumulator = 1;
                    }
                    this.slideNumberController.setSlideNumber(this.digitAccumulator);
                    this.jumpToSlide(this.digitAccumulator);
                } else {
                    debugMessage(kDebugShowController_OnKeyPress, "- can't do it, we're in hyperlinks only mode");
                }
                break;
            }
            // fall through

        case kKeyCode_N:
        case kKeyCode_Space:
        case kKeyCode_DownArrow:
        case kKeyCode_RightArrow:
        case kKeyCode_PageDown:
            // advance to next build...
            this.advanceToNextBuild("onKeyPress");
            break;

        case kKeyCode_RightArrow + kKeyModifier_Shift:
        case kKeyCode_CloseBracket:
            // advance and skip build...
            this.advanceAndSkipBuild("onKeyPress");
            break;

        case kKeyCode_DownArrow + kKeyModifier_Shift:
        case kKeyCode_PageDown + kKeyModifier_Shift:
        case kKeyCode_CloseBracket:
        case kKeyCode_Equal + kKeyModifier_Shift:
        case kKeyCode_Equal:
        case kKeyCode_Plus:
            // advance to next slide...
            this.advanceToNextSlide("onKeyPress");
            break;

        case kKeyCode_LeftArrow + kKeyModifier_Shift:
        case kKeyCode_PageUp + kKeyModifier_Shift:
        case kKeyCode_OpenBracket:
            // go back to previous build...
            this.goBackToPreviousBuild("onKeyPress");
            break;

        case kKeyCode_P:
        case kKeyCode_PageUp:
        case kKeyCode_LeftArrow:
        case kKeyCode_UpArrow:
        case kKeyCode_UpArrow + kKeyModifier_Shift:
        case kKeyCode_Hyphen:
        case kKeyCode_Minus:
            // go back to previous slide...
            this.goBackToPreviousSlide("onKeyPress");
            break;

        case kKeyCode_Delete:
            digitEncountered = true;
            if (this.accumulatingDigits) {
                if (this.digitAccumulator < 10) {
                    if (this.slideNumberTimeout) {
                        clearTimeout(this.slideNumberTimeout);
                    }
                    this.slideNumberTimeout = setTimeout(this.hideAndResetSlideNumberController.bind(this), 0);
                }
                else {
                    if (this.slideNumberTimeout) {
                        clearTimeout(this.slideNumberTimeout);
                    }
                    this.slideNumberTimeout = setTimeout(this.hideAndResetSlideNumberController.bind(this), 7000);

                    var digit = this.digitAccumulator.toString();
                    this.digitAccumulator = parseInt(digit.substring(0, digit.length - 1));
                    this.slideNumberController.setSlideNumber(this.digitAccumulator);
                }
            }
            break;

        case kKeyCode_Home:
            // go back to first slide...
            if (this.script.showMode != kShowModeHyperlinksOnly) {
                this.jumpToSlide(1);
            } else {
                debugMessage(kDebugShowController_OnKeyPress, "- can't do it, we're in hyperlinks only mode");
            }
            break;

        case kKeyCode_End:
            // go back to last slide...
            if (this.script.showMode != kShowModeHyperlinksOnly) {
                this.jumpToSlide(this.script.slideCount);
            } else {
                debugMessage(kDebugShowController_OnKeyPress, "- can't do it, we're in hyperlinks only mode");
            }
            break;

        default:
            if (this.slideNumberTimeout) {
                clearTimeout(this.slideNumberTimeout);
            }
            this.slideNumberTimeout = setTimeout(this.hideAndResetSlideNumberController.bind(this), 7000);

            if ((key >= kKeyCode_0) && (key <= kKeyCode_9)) {
                if (this.slideNumberDisplay.isShowing) {
                    this.slideNumberDisplay.hide();
                }
                
                digitEncountered = true;
                if (this.accumulatingDigits === false) {
                    // digit entered, start accumulating digits...
                    this.accumulatingDigits = true;
                    this.digitAccumulator = 0;
                }

                if (this.digitAccumulator.toString().length < 4) {
                    this.digitAccumulator *= 10;
                    this.digitAccumulator += (key - kKeyCode_0);
                    
                    this.slideNumberController.setSlideNumber(this.digitAccumulator);
                    if (!this.slideNumberController.isShowing) {
                        this.slideNumberController.show();
                    }
                }
            }
            else {
                digitEncountered = true;
            }
            break;
        }

        if (this.accumulatingDigits && (digitEncountered === false)) {
            // non-digit entered, stop accumulating digits...
            //this.accumulatingDigits = false;
            //this.digitAccumulator = 0;
        }
    },
    
    hideAndResetSlideNumberController: function() {
        if (this.slideNumberTimeout) {
            clearTimeout(this.slideNumberTimeout);
        }
        
        this.accumulatingDigits = false;
        this.digitAccumulator = 0;
        this.slideNumberController.hide();
    },
    
    hideSlideNumberDisplay: function() {
        this.slideNumberDisplay.hide();
    },

    toggleFullscreen: function() {
        // IE does not support fullscreen mode, return for now
        if (isIE) {
            return;
        }

        setTimeout((function() {
            this.displayManager.stageArea.style.opacity = 0;
        }).bind(this), 0);

        // hide hud immediately
        this.displayManager.hideHUD(true);

        if (document.webkitIsFullScreen || document.mozFullScreen) {
            this.isFullscreen = false;
            (document.webkitCancelFullScreen && document.webkitCancelFullScreen())
                    || (document.mozCancelFullScreen && document.mozCancelFullScreen());
        } else {
            this.isFullscreen = true;
            (document.body.webkitRequestFullScreen && document.body.webkitRequestFullScreen())
                    || (document.body.mozRequestFullScreen && document.body.mozRequestFullScreen());
        }
    },

    // State Management
    // ================
    changeState: function(newState) {
        if (newState != this.state) {
            //this.accumulatingDigits = false;
            //this.digitAccumulator = 0;

            this.leavingState();
            this.state = newState;
            this.enteringState();
        }
    },

    leavingState: function() {
        switch (this.state) {
        case kShowControllerState_Stopped:
            break;

        case kShowControllerState_Starting:
            break;

        case kShowControllerState_SettingUpScene:
            break;

        case kShowControllerState_IdleAtFinalState:
            break;

        case kShowControllerState_IdleAtInitialState:
            break;

        case kShowControllerState_WaitingToJump:
            break;

        case kShowControllerState_ReadyToJump:
            break;

        case kShowControllerState_WaitingToPlay:
            this.displayManager.hideWaitingIndicator();
            break;

        case kShowControllerState_ReadyToPlay:
            break;

        case kShowControllerState_Playing:
            break;
        }
    },

    enteringState: function() {
        switch (this.state) {
        case kShowControllerState_Stopped:
            break;

        case kShowControllerState_Starting:
            this.displayManager.showWaitingIndicator();
            break;

        case kShowControllerState_SettingUpScene:
            break;

        case kShowControllerState_IdleAtFinalState:
            // unload slide cache not next to the current slide
            this.unloadTextures();

        case kShowControllerState_IdleAtInitialState:
            this.updateSlideNumber();

            runInNextEventLoop(this.doIdleProcessing.bind(this));
            break;

        case kShowControllerState_WaitingToJump:
            // don't show spinner here, do it in pollForSceneToLoad after a few
            // polls so the spinner doesn't come up right away
            break;

        case kShowControllerState_ReadyToJump:
            break;

        case kShowControllerState_WaitingToPlay:
            this.displayManager.showWaitingIndicator();
            break;

        case kShowControllerState_ReadyToPlay:
            break;

        case kShowControllerState_Playing:
            break;
        }
    },

    preloadTextures: function() {
        var script = this.script;
        var sceneIndexToUse = this.currentSceneIndex;

        if (this.state === kShowControllerState_IdleAtFinalState) {
            if (sceneIndexToUse < script.numScenes - 1) {
                sceneIndexToUse = sceneIndexToUse + 1;
            } else if (script.loopSlideshow) {
                sceneIndexToUse = 0;
            }
        }

        // preload textures
        this.textureManager.loadScene(sceneIndexToUse);
    },

    unloadTextures: function() {
        var script = this.script;
        var sceneIndexToUse = this.currentSceneIndex;

        if (this.state === kShowControllerState_IdleAtFinalState) {
            if (sceneIndexToUse < script.numScenes - 1) {
                sceneIndexToUse = sceneIndexToUse + 1;
            } else if (script.loopSlideshow) {
                sceneIndexToUse = 0;
            }
        }

        var currentSlideIndex = script.slideIndexFromSceneIndexLookup[sceneIndexToUse];

        var slideCache = this.textureManager.slideCache;

        for (var index in slideCache) {
            // detect current slide index and its previous and next slide in slideCache buffer
            if (index < currentSlideIndex - 1 || index > currentSlideIndex + 1) {
                // remove slide cache
                var cache = slideCache[index];
                for (var textureId in cache.textureAssets) {
                    var canvas = cache.textureAssets[textureId];
                    if (canvas) {
                        // clear canvas object
                        var context = canvas.getContext("2d");

                        if (context) {
                            context.clearRect(0, 0, canvas.width, canvas.height);
                        }

                        // remove reference
                        delete cache.textureAssets[textureId];
                    }
                }

                delete this.textureManager.slideCache[index].textureAssets;
                delete this.textureManager.slideCache[index].textureRequests;
                delete this.textureManager.slideCache[index].requested;

                // call internal pdf document destroy method
                if (cache.pdf) {
                    cache.pdf.destroy();
                    delete this.textureManager.slideCache[index].pdf;
                }

                // finally remove slide cache reference
                delete this.textureManager.slideCache[index];
            }
        }
    },

    doIdleProcessing: function() {
        // preload textures for next slide if applicable
        this.preloadTextures();

        if (this.queuedUserAction != null) {
            // executing queued user action...
            this.queuedUserAction();
            this.queuedUserAction = null;
        } else {
            var stage = this.stageManager.stage;

            if (stage.childNodes.length !== 0) {
                this.updateNavigationButtons();
            }
        }

        // create hyperlink in setTimeout using background thread
        clearTimeout(this.createHyperlinksForCurrentStateTimeout);
        this.createHyperlinksForCurrentStateTimeout = setTimeout((function() {this.createHyperlinksForCurrentState("idle");}).bind(this), 100);
    },

    truncatedSlideIndex: function(slideIndex) {
        return this.truncatedIndex(slideIndex, this.script.lastSlideIndex, this.script.loopSlideshow);
    },

    truncatedSceneIndex: function(sceneIndex) {
        return this.truncatedIndex(sceneIndex, this.script.lastSceneIndex, this.script.loopSlideshow);
    },

    truncatedIndex: function(index, lastIndex, isLooping) {
        if (index < 0) {
            if (isLooping) {
                index = index + lastIndex + 1;
            } else {
                index = -1;
            }
        } else if (index > lastIndex) {
            if (isLooping) {
                index = index - lastIndex - 1;
            } else {
                index = -1;
            }
        }
        return index;
    },

    advanceToNextBuild: function(context) {
        // do not proceed if the script is not available
        if (!this.script) {
            return false;
        }

        if (this.script.showMode === kShowModeHyperlinksOnly && context != "currentSceneDidComplete") {
            return false;
        }

        if (this.displayManager.infoPanelIsShowing) {
            return false;
        }

        var result = false;

        switch (this.state) {
        case kShowControllerState_IdleAtFinalState:
            if (this.nextSceneIndex === -1) {
                if (this.delegate.getKPFJsonStringForShow) {
                    this.stopSoundTrack();
                    this.exitShow();
                } else {
                    this.stopSoundTrack();
                    break;
                }
            }
            // idle on final state, jump to next scene
            result = true;
            this.jumpToScene(this.nextSceneIndex, true);
            break;

        case kShowControllerState_IdleAtInitialState:
            if (this.currentSceneIndex >= this.script.numScenes) {
                if (this.script.loopSlideshow) {
                    // we're at the end but this IS a looping show, jump to start
                    result = true;
                    this.jumpToScene(0, false);
                } else {
                    if (this.delegate.getKPFJsonStringForShow) {
                        this.stopSoundTrack();
                        this.exitShow();
                    } else {
                        this.stopSoundTrack();
                        break;
                    }
                }
            } else {
                // we're sitting idle on initial state, preload next scene and play current scene
                result = true;
                this.playCurrentScene();
            }
            break;

        default:
            debugMessage(kDebugShowController_AdvanceToNextBuild, "nextSceneIndex: " + this.nextSceneIndex + " can't advance now, not in an idle state (currently in '" + this.state + "' state), queue up action to run in next idle time");

            if (this.queuedUserAction == null) {
                result = true;
                this.queuedUserAction = this.advanceToNextBuild.bind(this, context);
            }
            break;
        }

        return result;
    },

    advanceToNextSlide: function(context) {
        // do not proceed if the script is not available
        if (!this.script) {
            return false;
        }

        if (this.script.showMode == kShowModeHyperlinksOnly) {
            return;
        }

        if (this.displayManager.infoPanelIsShowing) {
            return;
        }

        var sceneIndexToUse = this.currentSceneIndex;

        switch (this.state) {
        case kShowControllerState_IdleAtFinalState:
            sceneIndexToUse = sceneIndexToUse + 1;
            // Fall through

        case kShowControllerState_IdleAtInitialState:
            var currentSlideIndex = this.scriptManager.slideIndexFromSceneIndex(sceneIndexToUse);
            var nextSlideIndex ;

            if (currentSlideIndex === this.script.slideCount - 1) {
                if (this.script.loopSlideshow) {
                    nextSlideIndex = 0;
                } else {
                    return;
                }
            } else {
                nextSlideIndex = this.currentSlideIndex + 1;
            }

            var sceneIndex = this.scriptManager.sceneIndexFromSlideIndex(nextSlideIndex);
            var event = this.script.events[sceneIndex];
            var automaticPlay = event.automaticPlay == 1 || event.automaticPlay == true;
            this.jumpToSlide(nextSlideIndex + 1, automaticPlay);

            break;

        default:
            debugMessage(kDebugShowController_AdvanceToNextSlide, "can't advance now, not in an idle state (currently in '" + this.state + "' state), queue up action to run in next idle time");

            if (this.queuedUserAction == null) {
                this.queuedUserAction = this.advanceToNextSlide.bind(this, context);
            }
            break;
        }
    },

    goBackToPreviousBuild: function(context) {
        // do not proceed if the script is not available
        if (!this.script) {
            return false;
        }

        // going back to previous build, remove all media cache
        this.resetMediaCache();

        if (this.script.showMode == kShowModeHyperlinksOnly) {
            return;
        }

        if (this.displayManager.infoPanelIsShowing) {
            return;
        }

        var sceneIndexToUse = this.currentSceneIndex;

        switch (this.state) {
        case kShowControllerState_IdleAtFinalState:
            sceneIndexToUse = sceneIndexToUse + 1;
            // Fall through

        case kShowControllerState_Playing:
        case kShowControllerState_IdleAtInitialState:
            var previousSceneIndex ;

            if (sceneIndexToUse === 0) {
                if (this.script.loopSlideshow) {
                    previousSceneIndex = this.script.events.length - 1;
                } else {
                    return;
                }
            } else {
                previousSceneIndex = sceneIndexToUse - 1;
            }
 
            this.jumpToScene(previousSceneIndex, false);

            break;

        default:
            debugMessage(kDebugShowController_GoBackToPreviousBuild, "can't go back now, not in an idle state (currently in '" + this.state + "' state)");

            if (this.queuedUserAction == null) {
                this.queuedUserAction = this.goBackToPreviousBuild.bind(this, context);
            }
            break;
        }
    },

    advanceAndSkipBuild: function(context) {
        // do not proceed if the script is not available
        if (!this.script) {
            return false;
        }

        if (this.script.showMode == kShowModeHyperlinksOnly) {
            return;
        }

        var sceneIndexToUse = this.currentSceneIndex;

        switch (this.state) {
            case kShowControllerState_IdleAtFinalState:
                sceneIndexToUse = sceneIndexToUse + 1;
                // Fall through
            case kShowControllerState_IdleAtInitialState:
                var nextSceneIndex;

                if (sceneIndexToUse >= this.script.numScenes - 1) {
                    if (this.script.loopSlideshow) {
                        nextSceneIndex = 0;
                    } else {
                        return;
                    }
                } else {
                    nextSceneIndex = sceneIndexToUse + 1;
                }

                this.jumpToScene(nextSceneIndex, false);

                break;

            default:
                debugMessage(kDebugShowController_GoBackToPreviousBuild, "can't go back now, not in an idle state (currently in '" + this.state + "' state)");

                if (this.queuedUserAction == null) {
                    this.queuedUserAction = this.advanceAndSkipBuild.bind(this, context);
                }
                break;
        }
    },

    goBackToPreviousSlide: function(context) {
        // do not proceed if the script is not available
        if (!this.script) {
            return false;
        }

        if (this.script.showMode == kShowModeHyperlinksOnly) {
            return;
        }

        if (this.displayManager.infoPanelIsShowing) {
            return;
        }

        var sceneIndexToUse = this.currentSceneIndex;

        switch (this.state) {
        case kShowControllerState_IdleAtFinalState:
            sceneIndexToUse = sceneIndexToUse + 1;
            // Fall through

        case kShowControllerState_Playing:
        case kShowControllerState_IdleAtInitialState:
            var currentSlideIndex = this.scriptManager.slideIndexFromSceneIndex(sceneIndexToUse);
            var sceneIndexForCurrentSlideIndex = this.scriptManager.sceneIndexFromSlideIndex(currentSlideIndex);
            var previousSlideIndex;

            if (currentSlideIndex === 0) {
                if (sceneIndexToUse > 0) {
                    // if we are not on first build of the slide, go back to first build of the slide
                    previousSlideIndex = 0;
                } else {
                    if (this.script.loopSlideshow) {
                        previousSlideIndex = this.script.slideCount - 1;
                    } else {
                        previousSlideIndex = 0;
                    }
                }
            } else if (currentSlideIndex === -1 && sceneIndexToUse > 0) {
                previousSlideIndex = this.script.slideCount - 1;
            } else {
                if (sceneIndexToUse > sceneIndexForCurrentSlideIndex) {
                    // if we are not on first build of the slide, go back to first build of the slide
                    previousSlideIndex = this.currentSlideIndex;
                } else {
                    // if we are on first build of the slide, go back to previous slide
                    previousSlideIndex = this.currentSlideIndex - 1;
                }
            }

            this.jumpToSlide(previousSlideIndex + 1);
            
            break;

        default:
            debugMessage(kDebugShowController_GoBackToPreviousSlide, "can't go back now, not in an idle state (currently in '" + this.state + "' state)");

            if (this.queuedUserAction == null) {
                this.queuedUserAction = this.goBackToPreviousSlide.bind(this, context);
            }
            break;
        }
    },

    calculatePreviousSceneIndex: function(sceneIndex) {
        if (sceneIndex == -1) {
            previousSceneIndex = -1;
        }
        else {
            previousSceneIndex = sceneIndex - 1;
        }

        return previousSceneIndex;
    },

    jumpToSlide: function(slideNumber, automaticPlay) {
        var slideIndex = slideNumber - 1;
        var sceneIndex = this.scriptManager.sceneIndexFromSlideIndex(slideIndex);

        // we are jumping to slide, remove all media cache
        this.resetMediaCache();

        // enable automatic play when the slide it advances to has automatic play
        // see <rdar://problem/12781266> Next slide does not auto advance when using keyboard to advance without transiti
        if (automaticPlay == null) {
            automaticPlay = false;
        }

        this.jumpToScene(sceneIndex, automaticPlay);
    },

    jumpToScene: function(sceneIndex, playAnimations) {
        this.lastSlideViewedIndex = this.scriptManager.slideIndexFromSceneIndex(this.currentSceneIndex);
        if (sceneIndex === -1) {
            return;
        }

        switch (this.state) {
            case kShowControllerState_Starting:
                // There is a bug in webkit - cursor not being able to change if sitting idle unless it has been moved
                // The workaround for the bug without moving the mouse cursor is to change DOM structure to force a redraw
                // References: http://code.google.com/p/chromium/issues/detail?id=26723
                var cssText = "position:absolute;background-color:transparent; left:0px; top:0px; width:" + this.displayManager.usableDisplayWidth +"px; height:" + this.displayManager.usableDisplayHeight + "px;";
                this.starting = true;
                this.maskElement = document.createElement("div");
                this.maskElement.setAttribute("style",cssText);
                document.body.appendChild(this.maskElement);

            case kShowControllerState_IdleAtInitialState:
            case kShowControllerState_IdleAtFinalState:
            case kShowControllerState_ReadyToJump:
                break;

        default:
            debugMessage(kDebugShowController_JumpToScene, "can't jump now, currently in '" + this.state + "' state which does not supports jumping...");
            return;
        }

        if (this.textureManager.isScenePreloaded(sceneIndex) === false) {
            this.changeState(kShowControllerState_WaitingToJump);

            // loadScene with callback handler and params
            var sceneToLoadInfo = {
                sceneIndex: sceneIndex,
                automaticPlay: playAnimations
            };

            this.waitForSceneToLoadTimeout = setTimeout(this.handleSceneDidNotLoad.bind(this, sceneToLoadInfo), kMaxSceneDownloadWaitTime);
            this.textureManager.loadScene(sceneIndex, this.handleSceneDidLoad.bind(this, sceneToLoadInfo));

            return;
        }

        this.changeState(kShowControllerState_SettingUpScene);

        runInNextEventLoop(this.jumpToScene_partThree.bind(this, sceneIndex, playAnimations));
    },

    handleSceneDidLoad: function(sceneToLoadInfo) {
        clearTimeout(this.waitForSceneToLoadTimeout);
        this.displayManager.setNextButtonEnabled(this.currentSceneIndex < (this.script.pageCount - 1));

        switch (this.state) {
        case kShowControllerState_WaitingToJump:
            this.changeState(kShowControllerState_ReadyToJump);
            this.jumpToScene_partTwo(sceneToLoadInfo.sceneIndex, sceneToLoadInfo.automaticPlay);
            break;

        default:
            break;
        }
    },

    handleSceneDidNotLoad: function(sceneToLoadInfo) {
        clearTimeout(this.waitForSceneToLoadTimeout);
        this.queuedUserAction = null;

        var tryAgain = this.promptUserToTryAgain(kUnableToReachiWorkTryAgain);

        if (tryAgain) {
            // restarting player with sceneIndex
            var currentUrl = window.location.href;
            var croppedUrl;
            var indexOfRestartParam = currentUrl.indexOf("&restartingSceneIndex");

            if (indexOfRestartParam === -1) {
                croppedUrl = currentUrl;
            } else {
                croppedUrl = currentUrl.substring(0, indexOfRestartParam);
            }

            var newUrl = croppedUrl + "&restartingSceneIndex=" + sceneToLoadInfo.sceneIndex;
            window.location.replace(newUrl);
        } else {
            this.changeState(kShowControllerState_IdleAtFinalState);
        }
    },

    jumpToScene_partTwo: function(sceneIndex, playAnimations) {
        this.changeState(kShowControllerState_SettingUpScene);

        // state changed (UI controls should disable), run partThree in next event loop
        runInNextEventLoop(this.jumpToScene_partThree.bind(this, sceneIndex, playAnimations));
    },

    jumpToScene_partThree: function(sceneIndex, playAnimations) {
        var delayBeforeNextPart = false;

        if (delayBeforeNextPart) {
            runInNextEventLoop(this.jumpToScene_partFour.bind(this, sceneIndex, playAnimations));
        } else {
            this.jumpToScene_partFour(sceneIndex, playAnimations);
        }
    },

    jumpToScene_partFour: function(sceneIndex, playAnimations) {
        this.displayScene(sceneIndex);

        if (this.starting) {
            // There is a bug in webkit - cursor not being able to change if sitting idle unless it has been moved
            // The workaround for the bug without moving the mouse cursor is to change DOM structure to force a redraw
            // References: http://code.google.com/p/chromium/issues/detail?id=26723
            if (this.maskElement != null) {
                document.body.removeChild(this.maskElement);
                this.maskElement = null;
                this.starting = false;
            }

            window.focus();
        }

        if (this.helpPlacard.isShowing) {
            this.helpPlacard.hide();
        }
        
        if (this.slideNumberDisplay.isShowing) {
            this.slideNumberDisplay.hide();
        }
        
        if (this.slideNumberController.isShowing) {
            if (this.slideNumberTimeout) {
                clearTimeout(this.slideNumberTimeout);
            }
            this.slideNumberTimeout = setTimeout(this.hideAndResetSlideNumberController.bind(this), 500);
        }

        if (playAnimations) {
            var script = this.script;

            if (script.showMode === kShowModeAutoplay) {
                var event = script.events[sceneIndex];
                var effects = event.effects;

                if (effects && effects.length > 0) {
                    var delay = effects[0].type === "transition" ? script.autoplayTransitionDelay : script.autoplayBuildDelay;

                    setTimeout((function(){this.playCurrentScene();}).bind(this), delay * 1000);
                } else {
                    this.playCurrentScene();
                }
            } else {
                this.playCurrentScene();
            }
        } else {
            this.changeState(kShowControllerState_IdleAtInitialState);

            if (this.isRecording && !this.isRecordingStarted) {
                this.narrationManager.start();
                this.isRecordingStarted = true;
            }
        }
    },

    displayScene: function(sceneIndex, hyperlinkEvent) {
        if (sceneIndex === -1) {
            return;
        }

        // remove all css
        this.animationManager.deleteAllAnimations();

        // clean up media cache if we are advancing to different slide
        var outgoingSlideIndex = this.scriptManager.slideIndexFromSceneIndex(this.currentSceneIndex);
        var incomingSlideIndex = hyperlinkEvent ? hyperlinkEvent.slideIndex : this.scriptManager.slideIndexFromSceneIndex(sceneIndex);

        if (outgoingSlideIndex !== incomingSlideIndex) {
            this.resetMediaCache();
        }

        // set currentSceneIndex
        this.setCurrentSceneIndexTo(sceneIndex);

        if (hyperlinkEvent) {
            this.playbackController.renderEvent(hyperlinkEvent);
        } else {
            var slideIndex = this.script.slideIndexFromSceneIndexLookup[sceneIndex];
            var slideId = this.script.slideList[slideIndex];

            var kpfEvent = new KPFEvent({
                "slideId": slideId,
                "slideIndex": slideIndex,
                "sceneIndex": sceneIndex,
                "event": this.script.events[sceneIndex],
                "animationSupported": this.animationSupported
            });

            this.playbackController.renderEvent(kpfEvent);
        }

        this.updateNavigationButtons();
    },

    setCurrentSceneIndexTo: function(sceneIndex) {
        this.currentSceneIndex = sceneIndex;
        this.assignNextSceneIndex();

        this.updateSlideNumber();
        this.updateNavigationButtons();
    },

    assignNextSceneIndex: function() {
        this.nextSceneIndex = this.calculateNextSceneIndex(this.currentSceneIndex);
    },

    calculateNextSceneIndex: function(sceneIndex) {
        var nextSceneIndex = this.calculateNextSceneIndex_internal(sceneIndex);

        return nextSceneIndex;
    },

    calculateNextSceneIndex_internal: function(sceneIndex) {
        var nextSceneIndex = -1;

        if (sceneIndex < this.script.lastSceneIndex) {
            nextSceneIndex = sceneIndex + 1;
        } else {
            if (this.script.loopSlideshow) {
                nextSceneIndex = 0;
            } else {
                nextSceneIndex = -1;
            }
        }

        return nextSceneIndex;
    },

    updateSlideNumber: function() {
        var adjustedSceneIndex = this.currentSceneIndex;

        if (this.state === kShowControllerState_IdleAtFinalState) {
            // because we're waiting at end state, we need to add one...
            adjustedSceneIndex = this.nextSceneIndex;
        }

        var newSlideIndex = this.scriptManager.slideIndexFromSceneIndex(adjustedSceneIndex);

        if (this.firstSlide) {
            this.displayManager.hideWaitingIndicator();

            runInNextEventLoop((function() {
                this.startSoundTrack();
                this.displayManager.clearLaunchMode();
            }).bind(this));
            this.firstSlide = false;
        }

        if (this.currentSlideIndex != newSlideIndex) {
            this.previousSlideIndex = this.currentSlideIndex;
            this.currentSlideIndex = newSlideIndex;

            this.displayManager.updateSlideNumber(this.currentSlideIndex + 1, this.script.slideCount);
            this.delegate.propertyChanged(kPropertyName_currentSlide, this.currentSlideIndex + 1);

            // fire SlideIndexDidChangeEvent
            document.fire(kSlideIndexDidChangeEvent, {
                slideIndex: this.currentSlideIndex
            });
        }
    },

    updateNavigationButtons: function() {
        var sceneIndexToUse = this.currentSceneIndex;

        if (this.state === kShowControllerState_IdleAtFinalState) {
            sceneIndexToUse++;
        }

        this.updateWindowHistory(sceneIndexToUse);

        var enableBackwardButton = false;
        var enableForwardButton = false

        if (this.script.lastSceneIndex === -1) {
            // this slideshow has only 1 slide with no builds, both buttons are
            // disabled
            enableForwardButton = false;
            enableBackwardButton = false;
        } else if (this.script.loopSlideshow) {
            // this is a looping slideshow, both buttons are ALWAYS enabled
            enableForwardButton = true;
            enableBackwardButton = true;
        } else {
            if (sceneIndexToUse > 0) {
                // sceneIndexToUse > 0, so enable backward button
                enableBackwardButton = true;
            }

            if (sceneIndexToUse === 0 && this.script.lastSceneIndex === 0) {
                // sceneIndexToUse & lastSceneIndex are both 0 - show with 1
                // slide with 1 build, so enable forward button
                enableForwardButton = true;
            } else if (this.currentSceneIndex < this.script.lastSceneIndex) {
                // currentSceneIndex < lastSceneIndex, so enable forward button
                enableForwardButton = true;
            } else if (this.currentSceneIndex === this.script.lastSceneIndex) {
                if (this.state === kShowControllerState_IdleAtInitialState) {
                    // currentSceneIndex === lastSceneIndex, but we're at the
                    // intitial state, so enable forward button
                    enableForwardButton = true;
                } else {
                    // currentSceneIndex === lastSceneIndex, and we're at the
                    // final state, so disable forward button
                    enableForwardButton = false;
                }
            } else {
                // currentSceneIndex > lastSceneIndex, show with 1 slide and no
                // builds, so disable forward button
                enableForwardButton = false;
            }
        }

        this.displayManager.setPreviousButtonEnabled(enableBackwardButton);
        this.displayManager.setNextButtonEnabled(enableForwardButton);
    },

    playCurrentScene: function(hyperlinkEventInfo) {
        var previousState = this.state;
        var sceneIndexToJump;
        var delay = 0;
        var duration = this.playbackController.eventOverallEndTime();

        this.changeState(kShowControllerState_Playing);

        this.clearAllHyperlinks();

        if (this.helpPlacard.isShowing) {
            this.helpPlacard.hide();
        }

        if (this.slideNumberDisplay.isShowing) {
            this.slideNumberDisplay.hide();
        }

        if (hyperlinkEventInfo) {
            sceneIndexToJump = hyperlinkEventInfo.sceneIndexToJump;

            // clean up media cache if we are playing hyperlink transition
            this.resetMediaCache();
        } else {
            sceneIndexToJump = this.nextSceneIndex;

            // clean up media cache if we are advancing to different slide
            var outgoingSlideIndex = this.scriptManager.slideIndexFromSceneIndex(this.currentSceneIndex);
            var incomingSlideIndex = this.scriptManager.slideIndexFromSceneIndex(sceneIndexToJump);

            if (outgoingSlideIndex !== incomingSlideIndex) {
                this.resetMediaCache();
            }

            // for transition, the delay time is no longer coded into animations.
            // set delay time and duration for automaticPlay transition
            if (this.playbackController.kpfEvent.event.automaticPlay == true && this.playbackController.kpfEvent.event.effects[0].type === "transition") {
                delay =  this.playbackController.kpfEvent.event.effects[0].beginTime;
                duration = this.playbackController.kpfEvent.event.effects[0].duration;
            }
        }

        if (this.animationSupported) {
            // animate events
            // Chrome chokes up in some animations if starting animations immediately after layer drawings all on the main thread
            // Start animations in a background thread will improve performance
            // see <rdar://problem/12636430> DEMO: Chrome performance issues with cube, flip, push
            clearTimeout(this.animateTimeout);

            var effects = this.playbackController.kpfEvent.event.effects;

            // if the event has no effects then no need to render effects
            if (effects.length === 0) {
                this.animateTimeout = setTimeout((function(){
                    setTimeout(this.currentSceneDidComplete.bind(this, sceneIndexToJump), duration * 1000 + 100);
                }).bind(this), delay * 1000);
            } else {
                var renderedEffects;
                // for performance consideration, render the effects as soon as we start playing so the effects are prepared and ready to animate
                // for blinds there is an issue with incoming particles on top of ougoing particles
                // since we don't want to show incoming particles before the animations start, we will render the effect later after delay as an exception case
                if (effects[0].type === "transition") {
                    if (isIE || isEdge) {
                        // Blinds fallback to dissolve on IE so ok to render here without seeing particle in reverse order
                        renderedEffects = this.playbackController.renderEffects();
                    } else {
                        if (effects[0].name != "com.apple.iWork.Keynote.BLTBlinds") {
                            renderedEffects = this.playbackController.renderEffects();
                        }
                    }
                }

                // account for transition delay and start animating the effects after transition delay
                this.animateTimeout = setTimeout((function(renderedEffects){
                    if (renderedEffects == null) {
                        renderedEffects = this.playbackController.renderEffects();
                    }
                    this.playbackController.animateEffects(renderedEffects);
                    setTimeout(this.currentSceneDidComplete.bind(this, sceneIndexToJump), duration * 1000 + 100);
                }).bind(this, renderedEffects), delay * 1000);
            }
        } else {
            var automatic = this.script.events[this.currentSceneIndex].automaticPlay;

            if (sceneIndexToJump === -1) {
                this.updateNavigationButtons();
                if (this.delegate.getKPFJsonStringForShow) {
                    if (automatic) {
                        setTimeout(this.exitShow.bind(this), 2000);
                    } else {
                        this.exitShow();
                    }
                } else {
                    this.changeState(kShowControllerState_IdleAtInitialState);
                }
            } else {
                // For IE9, animate current event means jump to next slide since there is no animated end state 
                if (automatic) {
                    // For IE9, do a flat 2 sec delay for any automatic event to jump to next scene
                    setTimeout(
                         (function(){
                             this.changeState(kShowControllerState_IdleAtInitialState);
                             this.jumpToScene(sceneIndexToJump, this.script.events[sceneIndexToJump].automaticPlay);
                         }).bind(this), 2000);
                } else {
                    // if it's not automatic then jump to next scene
                    this.changeState(kShowControllerState_IdleAtInitialState);
                    setTimeout(this.jumpToScene.bind(this, sceneIndexToJump, this.script.events[sceneIndexToJump].automaticPlay), 100);
                }
            }
        }

    },

    currentSceneDidComplete: function(sceneIndexToJump) {
        var script = this.script;
        var showMode = script.showMode;

        // hide slide number display after playing current scene
        if (this.slideNumberDisplay.isShowing) {
            this.slideNumberDisplay.hide();
        }

        // change state to final after animation completed
        this.changeState(kShowControllerState_IdleAtFinalState);

        if (showMode == kShowModeHyperlinksOnly || (sceneIndexToJump != -1 && sceneIndexToJump != this.nextSceneIndex)) {
            // if the show mode is hyperlink only
            // or if we play hyperlink transition that jumps to a slide which is not its next slide
            // then jump to the slide after transition
            var event = script.events[sceneIndexToJump];
            var automaticPlay = event.automaticPlay == 1 || event.automaticPlay == true;
            this.jumpToScene(sceneIndexToJump, automaticPlay);
        } else if (this.nextSceneIndex === -1) {
            // if next index is -1
            this.updateNavigationButtons();
            if (this.delegate.getKPFJsonStringForShow) {
                this.stopSoundTrack();
                this.exitShow();
            } else {
                this.stopSoundTrack();
            }
        } else if (script.events[this.nextSceneIndex].automaticPlay || showMode === kShowModeAutoplay) {
            // invoking advanceToNextBuild() on next event loop
            runInNextEventLoop(this.advanceToNextBuild.bind(this, "currentSceneDidComplete"));
        }
    },

    resetMediaCache: function() {
        this.resetMovieCache();
        this.resetAudioCache();
    },

    resetMovieCache: function() {
        for (var movieId in this.movieCache) {
            delete this.movieCache[movieId].videoElement;
            delete this.movieCache[movieId];
        }

        this.movieCache = null;
    },

    resetAudioCache: function() {
        for (var audioId in this.audioCache) {
            this.audioCache[audioId].pause();
            this.audioCache[audioId].src = "";

            delete this.audioCache[audioId];
        }
        this.audioCache = null;
    },

    updateWindowHistory: function(sceneIndex) {
        // update url
        if (typeof(window.history.replaceState) != "undefined") {
            var currentUrl = document.URL.split("?");
            var fragments = currentUrl[0].split("#");
            if (window.location.protocol !== "file:") {
                window.history.replaceState(null, "Keynote", fragments[0] + "#" + sceneIndex + (currentUrl[1] ? "?" + currentUrl[1] : ""));
            }
        }
    },

    startSoundTrack: function() {
        if (gMode === kModeMobile) {
            return;
        }

        if (this.script.soundtrack == null) {
            return;
        }

        if (this.script.soundtrack.tracks == null) {
            return;
        }

        if (this.script.soundtrack.mode === kSoundTrackModeOff) {
            return;
        }

        this.currentSoundTrackIndex = 0;
        this.playNextItemInSoundTrack();
    },

    stopSoundTrack: function() {
        if (this.soundTrackPlayer) {
            this.soundTrackPlayer.stopObserving("ended");
            this.soundTrackPlayer.pause();
            this.soundTrackPlayer = null;
        }
    },

    playNextItemInSoundTrack: function() {
        var soundtrackUrl = this.script.soundtrack.tracks[this.currentSoundTrackIndex];

        this.soundTrackPlayer = new Audio();
        this.soundTrackPlayer.src = "../" + soundtrackUrl;
        this.soundTrackPlayer.volume = this.script.soundtrack.volume;
        this.soundTrackPlayer.observe("ended", this.soundTrackItemDidComplete.bind(this), false);
        this.soundTrackPlayer.load();
        this.soundTrackPlayer.play();
    },

    soundTrackItemDidComplete: function() {
        // check to see if there's anything else to play
        this.currentSoundTrackIndex++;
        if (this.currentSoundTrackIndex < this.script.soundtrack.tracks.length) {
            this.playNextItemInSoundTrack();
        } else {
            if (this.script.soundtrack.mode === kSoundTrackModePlayOnce) {
                this.soundTrackPlayer = null;
            } else if (this.script.soundtrack.mode === kSoundTrackModeLooping) {
                // nope, but we're in loop mode so take it from the top
                this.startSoundTrack();
            }
        }
    },

    processHyperlink: function(hyperlink) {
        var hyperlinkUrl = hyperlink.url;
        var hyperlinkEffect;

        // perform hyperlink jump
        if (hyperlinkUrl.indexOf("?slide=") === 0) {
            var key = hyperlinkUrl.substring(7);
            var newSlideIndex = -1;

            if (key === "first") {
                newSlideIndex = 0;
            } else if (key === "last") {
                newSlideIndex = this.script.slideCount - 1;
            } else {
                var sceneIndexToUse = this.currentSceneIndex;
                var nextSlideIndex = -1;
                switch (this.state) {
                    case kShowControllerState_IdleAtFinalState:
                        sceneIndexToUse = sceneIndexToUse + 1;
                    case kShowControllerState_IdleAtInitialState:
                        var currentSlideIndex = this.scriptManager.slideIndexFromSceneIndex(sceneIndexToUse);
                        if (key === "next") {
                            if (currentSlideIndex === this.script.slideCount - 1) {
                                if (this.script.loopSlideshow) {
                                    nextSlideIndex = 0;
                                } else {
                                    if (this.delegate.getKPFJsonStringForShow) {
                                        this.exitShow();
                                    }
                                }
                            } else {
                                nextSlideIndex = currentSlideIndex + 1;
                            }
                        } else if (key === "previous") {
                            if (currentSlideIndex === 0) {
                                if (this.script.loopSlideshow) {
                                    nextSlideIndex = this.script.slideCount - 1;
                                } else {
                                    nextSlideIndex = 0;
                                }
                            } else {
                                nextSlideIndex = currentSlideIndex - 1;
                            }
                        }
                        break;

                    default:
                        break;
                }
                newSlideIndex = nextSlideIndex;

            }

            if (newSlideIndex != -1) {
                this.jumpToHyperlinkSlide(newSlideIndex, hyperlink);
            }
        } else if (hyperlinkUrl.indexOf("?slideid=") === 0) {
            // find by slideId
            var slideId = hyperlinkUrl.substring(9);
            var slideList = this.script.slideList;
            var newSlideIndex = -1;

            for (var i = 0, length = slideList.length; i < length; i++) {
                if (slideList[i] === slideId) {
                    newSlideIndex = i;
                    break;
                }
            }

            if (newSlideIndex != -1) {
                this.jumpToHyperlinkSlide(newSlideIndex, hyperlink);
            }
        } else if (hyperlinkUrl.indexOf("?action=retreat") === 0) {
            // jump to the last slide viewed
            if (this.lastSlideViewedIndex != -1) {
                this.jumpToHyperlinkSlide(this.lastSlideViewedIndex, hyperlink);
            }
        } else if (hyperlinkUrl.indexOf("?action=exitpresentation") === 0) {
            // exit show
            this.exitShow();
        } else if (hyperlinkUrl.indexOf("http:") === 0 || hyperlinkUrl.indexOf("https:") === 0) {
            // jump to a web page
            window.open(hyperlinkUrl, "_blank", null);
        } else if (hyperlinkUrl.indexOf("mailto:") === 0) {
            // email link
            window.location = hyperlinkUrl;
        }
    },

    jumpToHyperlinkSlide: function(slideIndexToJump, hyperlink) {
        //var hyperlinkEffects = hyperlink.effects;
        var hyperlinkEvents = hyperlink.events;
        var sceneIndexToJump = this.script.sceneIndexFromSlideIndexLookup[slideIndexToJump];

        if (hyperlinkEvents) {
            // if hyperlink has effect then display scene and use hyperlink event
            var slideIdToJump = this.script.slideList[slideIndexToJump];
            var hyperlinkEvent = hyperlinkEvents[slideIdToJump];

            if (hyperlinkEvent) {
                var sceneIndexOfHyperlink = this.currentSceneIndex;

                switch (this.state) {
                    case kShowControllerState_IdleAtFinalState:
                        if (sceneIndexOfHyperlink < this.script.numScenes - 1) {
                            sceneIndexOfHyperlink = sceneIndexOfHyperlink + 1;
                        } else {
                            if (this.script.loopSlideshow) {
                                sceneIndexOfHyperlink = 0;
                            }
                        }
                    case kShowControllerState_IdleAtInitialState:
                        var slideIndexOfHyperlink = this.script.slideIndexFromSceneIndexLookup[sceneIndexOfHyperlink];
                        var slideIdOfHyperlink = this.script.slideList[slideIndexOfHyperlink];

                        var kpfEvent = new KPFEvent({
                            "slideId": slideIdOfHyperlink,
                            "slideIndex": slideIndexOfHyperlink,
                            "sceneIndex": sceneIndexOfHyperlink,
                            "event": hyperlinkEvent,
                            "animationSupported": this.animationSupported
                        });

                        // display scene that contains hyperlink and use hyperlink event
                        this.displayScene(sceneIndexOfHyperlink, kpfEvent);

                        // play current hyperlink scene
                        this.playCurrentScene({"sceneIndexToJump": sceneIndexToJump});

                        break;

                    default:
                        return;
                }
            } else {
                // if no hyperlink effect, just jump to slide
                var event = this.script.events[sceneIndexToJump];
                var automaticPlay = event.automaticPlay == 1 || event.automaticPlay == true;
                this.jumpToSlide(slideIndexToJump + 1, automaticPlay); 
            }
        } else {
            // if no hyperlink effect, just jump to slide
            var event = this.script.events[sceneIndexToJump];
            var automaticPlay = event.automaticPlay == 1 || event.automaticPlay == true;
            this.jumpToSlide(slideIndexToJump + 1, automaticPlay);
        }
    },

    addMovieHyperlink: function(targetRectangle, url) {
        var newHyperlink = {
            targetRectangle: targetRectangle,
            url: url
        };
        this.movieHyperlinks.push(newHyperlink);
    },

    clearMovieHyperlinks: function() {
        delete this.movieHyperlinks;
        this.movieHyperlinks = new Array();
    },

    clearAllHyperlinks: function() {
        this.stageManager.clearAllHyperlinks();

        delete this.activeHyperlinks;
        this.activeHyperlinks = new Array();
    },

    findHyperlinkAtCoOrds: function(showCoOrds) {
        var numHyperlinks = this.activeHyperlinks != null ? this.activeHyperlinks.length : 0;

        for (var i = numHyperlinks; i > 0; i--) {
            var hyperlink = this.activeHyperlinks[i - 1];
            var hyperlinkRect = hyperlink.targetRectangle;

            hyperlinkLeft = Math.floor(hyperlinkRect.x);
            hyperlinkTop = Math.floor(hyperlinkRect.y);
            hyperlinkRight = hyperlinkLeft + Math.floor(hyperlinkRect.width);
            hyperlinkBottom = hyperlinkTop + Math.floor(hyperlinkRect.height);

            if ((showCoOrds.pointX >= hyperlinkLeft) && (showCoOrds.pointX <= hyperlinkRight)
                    && (showCoOrds.pointY >= hyperlinkTop) && (showCoOrds.pointY <= hyperlinkBottom)) {
                return hyperlink;
            }
        }

        return null;
    },

    createHyperlinksForCurrentState: function(context) {
        var sceneIndexOfHyperlinks = -1;

        switch (this.state) {
        case kShowControllerState_IdleAtInitialState:
            // use current scene index
            sceneIndexOfHyperlinks = this.currentSceneIndex;
            break;

        case kShowControllerState_IdleAtFinalState:
            // idle at final state, grab hyperlink from appropriate scene
            if (this.currentSceneIndex < this.script.lastSceneIndex) {
                sceneIndexOfHyperlinks = this.currentSceneIndex + 1;
            } else {
                // check if hyperlinks only mode
                if (this.script.showMode == kShowModeHyperlinksOnly) {
                    sceneIndexOfHyperlinks = this.currentSceneIndex;
                } else {
                    // check if loop slide show
                    if (this.script.loopSlideshow) {
                        sceneIndexOfHyperlinks = 0;
                    }
                }
            }
            break;

        default:
            break;
        }

        if (sceneIndexOfHyperlinks != -1) {
            this.clearAllHyperlinks();
            this.createHyperlinks(sceneIndexOfHyperlinks);
        }
    },

    createHyperlinks: function(hyperlinkSceneIndex) {
        if (hyperlinkSceneIndex === -1) {
            return;
        }

        var eventTimeLine = this.script.events[hyperlinkSceneIndex];
        if (eventTimeLine == null) {
            return;
        }

        var hyperlinks = eventTimeLine.hyperlinks;
        if (hyperlinks == null) {
            return;
        }

        var numHyperlinks = hyperlinks.length;
        var iHyperlink;
        var kMinHyperlinkWidth = 150;
        var kMinHyperlinkHeight = 50;
        var showWidth = this.displayManager.showWidth;
        var showHeight = this.displayManager.showHeight;

        for (iHyperlink = 0; iHyperlink < numHyperlinks; iHyperlink++) {
            var hyperlink = hyperlinks[iHyperlink];
            var hyperlinkRect = hyperlink.targetRectangle;

            var activeHyperlink = {
                targetRectangle: hyperlinkRect,
                events: hyperlink.events,
                url: hyperlink.url
            };

            var spaceOnLeft = hyperlinkRect.x;
            var spaceOnTop = hyperlinkRect.y;
            var spaceOnRight = showWidth - (hyperlinkRect.x + hyperlinkRect.width);
            var spaceOnBottom = showHeight - (hyperlinkRect.y + hyperlinkRect.top);

            if (gMode === kModeMobile) {
                if (hyperlinkRect.width < kMinHyperlinkWidth) {
                    var deltaWidth = kMinHyperlinkWidth - hyperlinkRect.width;
                    var leftShift = deltaWidth / 2;
                    var rightShift = deltaWidth / 2;

                    if (spaceOnLeft < leftShift) {
                        leftShift = spaceOnLeft;
                    } else if (spaceOnRight < rightShift) {
                        leftShift = leftShift + (rightShift - spaceOnRight);
                    }

                    activeHyperlink.targetRectangle.x -= leftShift;
                    activeHyperlink.targetRectangle.width += deltaWidth;
                }

                if (hyperlinkRect.height < kMinHyperlinkHeight) {
                    var deltaHeight = kMinHyperlinkHeight - hyperlinkRect.height;
                    var topShift = deltaHeight / 2;
                    var bottomShift = deltaHeight / 2;

                    if (spaceOnTop < topShift) {
                        topShift = spaceOnTop;
                    } else if (spaceOnBottom < bottomShift) {
                        topShift = topShift + (rightShift - spaceOnRight);
                    }

                    activeHyperlink.targetRectangle.y -= topShift;
                    activeHyperlink.targetRectangle.height += deltaHeight;
                }
            }
            this.stageManager.addHyperlink(activeHyperlink.targetRectangle);
            this.activeHyperlinks[iHyperlink] = activeHyperlink;
        }

        if (this.movieHyperlinks.length > 0) {
            for (var iMovieHyperlink = 0; iMovieHyperlink < this.movieHyperlinks.length; iMovieHyperlink++) {
                var movieHyperlink = this.movieHyperlinks[iMovieHyperlink];

                this.stageManager.addHyperlink(movieHyperlink.targetRectangle);
                this.activeHyperlinks[iHyperlink++] = movieHyperlink;
            }
        }
    }

});
