// CourseCast Viewer
// Copyright 2007 Panopto Inc
// All rights reserved.  Reuse and redistribution strictly prohibited.

// globals
var g_urlDeliveryInfo = "DeliveryInfo.aspx";

var g_urlNoteTogglePublic = "Notes/TogglePublic.aspx";
var g_urlNoteSubmit = "Notes/Note_Create.aspx";
var g_urlNoteUpdate = "Notes/Note_Update.aspx";
var g_urlNoteDelete = "Notes/Note_Delete.aspx";

var g_urlSearchResults = "Search/Results.aspx";

var g_urlImage = "Image.aspx";
var g_urlThumb = "Thumb.aspx";

var g_dMinViewerHeight = 500;
var g_dMinViewerWidth_Object = 930;
var g_dMinViewerWidth_NoObject = 700;
var g_dMinViewerWidth = g_dMinViewerWidth_Object;
var g_dScrollbarHeight = 17;

var g_fLeftPaneWidthPerc = .37;
var g_fRightPaneWidthPerc = .63;

var g_dBorderWidth = 1;

var g_dMinEventViewerHeight = 200;
var g_dMinEventViewerWidth = 340;

var g_fOffsetThreshold = 5.0;

var g_dThumbnailHeight = 218;
var g_dThumbnailWidth = 250;

var g_dQuestionsHeight = 110;

var g_dContainerSpacing = 10;

// Global reference to viewer object for media player event handler access.
var g_pViewer;

function PanoptoViewer(el, viewerParams)
{
    var m_el = el;  // our root element
    
    // member controls
    var m_pVideo = null;          // presenter video stream
    var m_pEventTabViewer = null; // table of contents control
    var m_pTabViewer = null;      // object region with tab control to switch between streams
    var m_pObjectVideo = null;    // object video player
    var m_pThumbnails = null;     // thumbnail view
    var m_pQuestions = null;      // question entry for broadcast

    // our delivery info object - parses xml and holds our data
    var m_pDelivery = null;

    // true if there are object streams to show
    var m_bHasObjectRegion = true;
    var m_bObjectRegionMaximized = false;

    var m_bHasTimestamps = true;
    var m_bShowThumbnails = true;

    // timer to update streams in broadcast viewer
    var m_pDeliveryInfoTimer;

    // timer used to update event streams and synchronize videos in non-broadcast case
    var m_pSyncTimer;

    // lastsetvideoposition keeps track of the last place we manually positioned the video
    // we need this to get around a bug where the video player sets its position slightly before
    // the specified time which results in the display of incorrect events
    var m_fLastSetVideoPosition = 0;

    // the version of the system
    this.version = viewerParams.Version;

    // the delivery id
    this.deliveryID = viewerParams.DeliveryID;

    // the session public id
    this.sessionPID;

    // whether the session is being broadcast live.  set in SetDelivery()
    this.isLive;

    // whether the viewer is standalone notes
    this.isStandaloneNotes = false;

    // the display names and bios of creators who contributed to the session
    this.contributors = viewerParams.Contributors;

    // the name of the logged in user, for notes
    this.userName = viewerParams.UserName;

    // the user whose notes will be shown in the notes tab (for public notes).  null -> default to user's login.
    this.notesUser = null;

    // the list of public notes streams (at load time)
    this.publicNotesUsers = viewerParams.PublicNotesStreams;

    // indicates whether the current user is permitted to make their personal notes stream public.
    // setting takes into account session group setting for viewers and current user's creator status.
    // set in SetDelivery()
    this.bAllowPublishNotes;

    // for login triggered from notes user <select>
    this.loginURL = viewerParams.LoginURL;
    
    // initialize the viewer root and hosted controls
    // this will leak if called multiple times!
    this.Initialize = function Initialize()
    {
        // create the events tab viewer
        m_pEventTabViewer = new EventTabViewer(document.getElementById("eventViewerDiv"), this);

        // create our archival video player
        if (g_bUsingSilverlight)
        {
            m_pVideo = new StartPlayer_0(document.getElementById("videoPlayerDiv"), this);
        }
        else
        {
            m_pVideo = new VideoPlayer(document.getElementById("videoPlayerDiv"), "archivalPlayer", true);
        }

        // create our thumbnails display
        m_pThumbnails = new Thumbnails(document.getElementById("thumbnails"), this);

        // create our tab viewer (boolean specifies large tabs)
        m_pTabViewer = new TabViewer(document.getElementById("tabViewer"), this, true);

        // create the questions entry
        m_pQuestions = new Questions(document.getElementById("questions"), this);

        // set event handler for window resize and clicking the thumbnail toggle button
        window.onresize = Function.createDelegate(this, this.Resize);
    }
    
    // get the data for the supplied delivery ID, then reconfigure the viewer
    // to display it.
    this.OpenDelivery = function OpenDelivery()
    {
        function deliveryCallback(pDocument, bSuccess)
        {
            if (!pDocument || !bSuccess)
            {
                alert("Error connecting to server. Please try refreshing the page later.");
            }
            else
            {
                var errorMessage = SelectSingleNodeValue(pDocument, "ErrorMessage");
                if (errorMessage)
                {
                    showMessage(errorMessage);

                    // Stop pinging DeliveryInfo when broadcast ends
                    if (m_pDeliveryInfoTimer)
                    {
                        clearInterval(m_pDeliveryInfoTimer);
                    }
                }
                else
                {
                    var pDelivery = new DeliveryInfo(pDocument);
                    // Apparently function calls from delegates do not preserve the instance pointer.
                    SetDelivery.call(this, pDelivery);
                }
            }
        }

        var params =
        {
            deliveryID      : this.deliveryID,
            invocationID    : viewerParams.InvocationID
        };

        CreateRequest(g_urlDeliveryInfo, params, Function.createDelegate(this, deliveryCallback));
    }

    // load information from the specified delivery into our viewers
    function SetDelivery(pDelivery)
    {
        // First load or broadcast hasn't started yet.
        if (m_pDelivery == null)
        {
            this.isLive = pDelivery.isLive;

            this.bAllowPublishNotes = pDelivery.bAllowPublishNotes;

            this.sessionPID = pDelivery.sessionPID;

            // Set session title in top bar.
            SetText(document.getElementById("courseTag"), pDelivery.sessionGroupShortName + ":");
            SetText(document.getElementById("sessionName"), pDelivery.sessionName);

            if (pDelivery.isStarted)
            {
                // Hide broadcast not started message (if shown) and display viewer div.
                showMessage(false);

                SetText(document.getElementById("infoSessionName"), pDelivery.sessionName);

                var el_sessionGroupName = document.getElementById("infoSessionGroupName");
                var sessionGroupName = "(" + pDelivery.sessionGroupShortName + ") " + pDelivery.sessionGroupLongName;
                SetText(el_sessionGroupName, sessionGroupName);

                var el_sessionGroupLink = document.getElementById("infoSessionGroupLink");
                el_sessionGroupLink.href = viewerParams.ApplicationPath + "/Student/CourseContents.aspx?id=" + pDelivery.sessionGroupPID;

                var el_sessionGroupAbstract = document.getElementById("sessionGroupAbstract");
                SetTextWithNewlineTranslation(el_sessionGroupAbstract, pDelivery.sessionGroupAbstract);

                // resize elements before setting contents
                OnResize();

                // render our event tab viewer and load the first event
                if (m_pEventTabViewer)
                {
                    m_pEventTabViewer.RenderContents(pDelivery.arrTOCEvents, pDelivery.arrTranscriptEvents);
                }

                // render the thumbnails
                if (m_pThumbnails)
                {
                    // if we have thumbnails to show, render them - otherwise hide the thumbnail pane
                    var thumbEl = document.getElementById("thumbnails");
                    m_bShowThumbnails = m_bHasTimestamps = (pDelivery.arrTOCEvents.length > 0);
                    if (m_bHasTimestamps)
                    {
                        m_pThumbnails.RenderContents(pDelivery.arrTOCEvents, pDelivery.sessionPID);
                        SetVisible(thumbEl, true);
                    }
                    else
                    {
                        SetVisible(thumbEl, false);
                    }
                }

                // render archival video player
                if (m_pVideo)
                {
                    // Can't act on events for broadcast case, set callbacks to null
                    var posChangedCallback = (pDelivery.isLive) ? null : Function.createDelegate(this, OnVideoPositionChanged);
                    var playStateChangedCallback = (pDelivery.isLive) ? null : Function.createDelegate(this, OnPlayStateChanged);

                    m_pVideo.Initialize(pDelivery.archivalStream, posChangedCallback, playStateChangedCallback);
                }

                // render the object material region and load the first event
                if (m_pTabViewer)
                {
                    m_pTabViewer.SetViews(pDelivery.arrObjectViews);
                }

                OnResize();

                m_pDelivery = pDelivery;
            }
            else
            {
                showMessage("Broadcast has not started yet.  The viewer will load when content is available.");
            }
            
            // Reload delivery info every 10sec in broadcast mode to pick up new streams (or to pick up a broadcast that has just begun)
            if (pDelivery.isLive)
            {
                // We may hit this point again if we're waiting for a broadcast to begin.
                if (!m_pDeliveryInfoTimer)
                {
                    m_pDeliveryInfoTimer = setInterval(Function.createDelegate(this, this.OpenDelivery), 10000);
                }
            }
            // Broadcast streams aren't seekable, so can't synchronize
            else
            {
                m_pSyncTimer = setInterval(Function.createDelegate(this, Synchronize), 500);
            }
        }
        // Broadcast update, refresh streams
        else
        {
            if (m_pTabViewer)
            {
                m_pTabViewer.SetViews(pDelivery.arrObjectViews);
            }
        }
    }

    this.GetHasObjectRegion = function() { return m_bHasObjectRegion; };

    this.SetHasObjectRegion = function(hasObjectRegion)
    {
        m_bHasObjectRegion = hasObjectRegion;
        m_pTabViewer.SetVisible(hasObjectRegion);

        var viewer = document.getElementById("viewer");
        var leftPane = document.getElementById("leftPane");
        var rightPane = document.getElementById("rightPane");
        var eventViewerDiv = document.getElementById("eventViewerDiv");

        if (hasObjectRegion)
        {
            // Show right pane and move event viewer below video
            leftPane.style.width = "37%";
            rightPane.style.display = "block";
            eventViewerDiv.style.position = "relative";
            eventViewerDiv.style.width = "";
            eventViewerDiv.style.paddingTop = "10px";

            g_dMinViewerWidth = g_dMinViewerWidth_Object;
        }
        else
        {
            // Collapse right pane and move event viewer to right of video
            leftPane.style.width = "100%";
            rightPane.style.display = "none";
            eventViewerDiv.style.position = "absolute";
            eventViewerDiv.style.paddingTop = "0px";

            g_dMinViewerWidth = g_dMinViewerWidth_NoObject;
        }

        viewer.style.minWidth = g_dMinViewerWidth + "px";

        this.Resize();
    }
    
    // enlarge / reduce object material tabviewer
    this.ToggleObjectRegionMaximized = function(maximized)
    {
        if (maximized != null)
        {
            m_bObjectRegionMaximized = maximized;
        }
        else
        {
            m_bObjectRegionMaximized = !m_bObjectRegionMaximized;
        }

        OnResize();
    }

    // Set thumbnail mode (show / hide).
    // Thumbnail display also depends on object region mode and presence of timestamps.
    function ToggleThumbnails(bShowThumbnails)
    {
        if (bShowThumbnails != null)
        {
            m_bShowThumbnails = bShowThumbnails;
        }
        else
        {
            m_bShowThumbnails = !m_bShowThumbnails;
        }
        
        m_pThumbnails.SetVisible(m_bShowThumbnails);
    }
    this.ToggleThumbnails = ToggleThumbnails;

    function OnVideoPositionChanged(fVidPos, bUserInitiated)
    {
        // Video position is not valid for broadcast, ignore.
        if (this.isLive) return;

        // if this was triggered by a user action then keep track of the last set position
        if (bUserInitiated)
        {
            m_fLastSetVideoPosition = fVidPos;
        }

        // determine the current item
        var iItem = GetCurrentItem(m_pDelivery.arrTOCEvents, fVidPos);
        m_pThumbnails.SelectThumbByIndex(iItem);

        // send the current state to the tabcontrol
        var pStatus = {      Time: fVidPos,
                        PlayState: m_pVideo.GetPlayState() };

        m_pTabViewer.UpdateStatus(pStatus, bUserInitiated);
        m_pEventTabViewer.UpdateStatus(pStatus);
    }
    
    var m_isBuffering = false;

    function StartBuffering()
    {
        // when buffering, pause our object video and keep track of our state
        if (!m_isBuffering)
        {
            m_pObjectVideo.SetPlayState("Paused");
            m_isBuffering = true;
        }
    }

    function EndBuffering()
    {
        // when done start playing the object video again
        if (m_isBuffering)
        {
            m_isBuffering = false;
            m_pObjectVideo.SetPlayState("Playing");
        }
    }
    
    // called when our archival video changes play state
    function OnPlayStateChanged(playState)
    {
        if (playState == "Buffering")
        {
            // if we are buffering, pause the object video
            StartBuffering();
        }
        else if (m_isBuffering)
        {
            // we are not now buffering but were buffering, so start playing the object video again  
            EndBuffering();
        }
        else
        {
            // we were not buffering and are now not buffering, so synchronize the object video's play state
            m_pObjectVideo.SetPlayState(playState);
        }
    }

    this.OnObjectVideoPlayStateChanged = function(playState)
    {
        // this handles the case where object video gets initialized after archival video is playing (clicking a tab)
        // it simply synchronizes play states when the object video is not buffering
        if ((playState != "Buffering") && (playState != m_pVideo.GetPlayState()))
        {
            m_pObjectVideo.SetPlayState(m_pVideo.GetPlayState());
        }
    }

    // needed to allow tabviewer to pass in our object video instance
    this.SetObjectVideoPlayer = function(player)
    {
        m_pObjectVideo = player;
    }

    this.SetVideoPosition = function(fVidPos)
    {
        m_pVideo.SetVideoPosition(fVidPos);
        OnVideoPositionChanged(fVidPos, true);
    }

    this.SelectObjectView = function(viewID)
    {
        m_pTabViewer.SelectView(viewID);
    }
    
    // callback to synchronize streams
    function Synchronize()
    {
        // take the later of the actual and lastsetvideo positions
        var fVidPos = m_pVideo.GetVideoPosition();
        var playState = m_pVideo.GetPlayState();

        // case where actual time is before the last set time
        if (fVidPos < m_fLastSetVideoPosition)
        {
            fVidPos = m_fLastSetVideoPosition;
        }

        OnVideoPositionChanged.call(this, fVidPos, false);
    }

    
    // event handler for window resize
    function OnResize()
    {
        if (m_el.style.display == "none") return;

        // determine our viewer height and set our outermost div - subtract 10 for the margin
        var viewerHeight = GetWindowHeight() - m_el.offsetTop - g_dContainerSpacing;
        viewerHeight = Math.max(g_dMinViewerHeight, viewerHeight);

        m_el.style.height = viewerHeight + "px";

        var viewerWidth = GetWindowWidth();
        viewerWidth = Math.max(g_dMinViewerWidth, viewerWidth);

        // set the height of our panes
        var leftPane = document.getElementById("leftPane");
        leftPane.style.height = viewerHeight + "px";

        var rightPane = document.getElementById("rightPane");
        rightPane.style.height = viewerHeight + "px";

        // call SetHeight for our events viewer and tab viewer
        // tab viewer is adjusted based on thumbnail display
        if (m_bHasObjectRegion)
        {
            var leftPaneWidth = (m_bObjectRegionMaximized ? g_dMinEventViewerWidth : (viewerWidth * g_fLeftPaneWidthPerc));

            var videoWidthAvailable = leftPaneWidth - (g_dContainerSpacing * 2);
            var videoHeightAvailable = viewerHeight - g_dMinEventViewerHeight;
            m_pVideo.OnResize(videoHeightAvailable, videoWidthAvailable);

            // Video may have shrunk to accommodate min event viewer height
            leftPaneWidth = m_pVideo.Width + (g_dContainerSpacing * 2);
            leftPane.style.width = leftPaneWidth + "px";
            rightPane.style.left = leftPaneWidth + "px";
            rightPane.style.width = viewerWidth - leftPaneWidth + "px";

            var eventTabViewerHeight = viewerHeight - m_pVideo.Height;
            eventTabViewerHeight = Math.max(g_dMinEventViewerHeight, eventTabViewerHeight);

            m_pEventTabViewer.SetHeight(eventTabViewerHeight);

            var tabViewerHeight = viewerHeight;
            if (m_bHasObjectRegion && !m_bObjectRegionMaximized && m_bShowThumbnails)
            {
                tabViewerHeight -= g_dThumbnailHeight;

                m_pThumbnails.SetVisible(true);
                m_pThumbnails.SetWidth(rightPane.offsetWidth - g_dContainerSpacing - (g_dBorderWidth * 2));
            }
            else
            {
                m_pThumbnails.SetVisible(false);
            }

            // Display questions tab for live broadcasts.
            if (g_pViewer.isLive)
            {
                tabViewerHeight -= (g_dQuestionsHeight + g_dContainerSpacing);

                m_pQuestions.SetVisible(true);
                m_pQuestions.OnResize(rightPane.offsetWidth - g_dContainerSpacing - (g_dBorderWidth * 2));
            }
            else
            {
                m_pQuestions.SetVisible(false);
            }

            var tabViewerWidth = rightPane.offsetWidth - g_dContainerSpacing;
            m_pTabViewer.OnResize(tabViewerHeight, tabViewerWidth);
        }
        else
        {
            m_pVideo.OnResize(viewerHeight, viewerWidth - g_dMinEventViewerWidth - g_dContainerSpacing * 3);
            m_pEventTabViewer.SetWidth(viewerWidth - m_pVideo.Width - g_dContainerSpacing * 3);
            // EventTabViewer reserves its own bottom margin, m_pVideo doesn't, so add margin height to make them match.
            m_pEventTabViewer.SetHeight(m_pVideo.Height + g_dContainerSpacing);
        }
    }
    this.Resize = OnResize;
}

// data object to store all info for a delivery
function DeliveryInfo(pXml)
{
    this.sessionPID;
    this.sessionName;
    this.sessionGroupPID;
    this.sessionGroupShortName;
    this.sessionGroupLongName;
    this.sessionGroupAbstract;
    this.isLive;
    
    this.arrTimestamps = new Array();
    this.arrPPTEvents = new Array();
    this.arrTOCEvents = new Array();
    this.arrTranscriptEvents = new Array();

    this.arrObjectViews = new Array();
    this.archivalStream;

    // Session and SessionGroup data
    this.sessionPID = SelectSingleNodeValue(pXml, "SessionPublicID");
    this.sessionName = SelectSingleNodeValue(pXml, "SessionName");
    this.sessionGroupPID = SelectSingleNodeValue(pXml, "SessionGroupPublicID");
    this.sessionGroupShortName = SelectSingleNodeValue(pXml, "SessionGroupShortName");
    this.sessionGroupLongName = SelectSingleNodeValue(pXml, "SessionGroupLongName");
    this.sessionGroupAbstract = SelectSingleNodeValue(pXml, "SessionGroupAbstract");

    // IsBroadcast indicates if the session was broadcast to begin with.
    // Only set viewer.isLive if the session is currently broadcasting,
    // else we're playing back a broadcast session.
    var bSessionIsBroadcast = SelectSingleNodeValue(pXml, "IsBroadcast", "bool");
    var bSessionIsOpen = SelectSingleNodeValue(pXml, "IsOpen", "bool");
    this.isLive = bSessionIsBroadcast && bSessionIsOpen;

    // Whether the session has started recording.  Used in broadcast case.
    this.isStarted = SelectSingleNodeValue(pXml, "IsStarted", "bool");

    this.bAllowPublishNotes = SelectSingleNodeValue(pXml, "AllowPublishNotes", "bool");

    // Timestamps
    var pTimestamps = SelectSingleNode(pXml, "Timestamps");
    var pArrTimestamps = SelectNodes(pTimestamps, "SimpleTimestamp");
    for (var i = 0; i < pArrTimestamps.length; i++)
    {
        var pEvent = ParseEvent(pArrTimestamps[i]);

        // skip this if we dont have time, eventtargetid, or sequencenumber
        if ((pEvent.Time != null) && (pEvent.EventTargetID != null) && (pEvent.SequenceNumber != null))
        {
            this.arrTimestamps.push(pEvent);
            if (pEvent.EventTargetType == "PowerPoint")
            {
                this.arrPPTEvents.push(pEvent);
                this.arrTOCEvents.push(pEvent);
            }
            else if (pEvent.EventTargetType == "ObjectVideo")
            {
                this.arrTOCEvents.push(pEvent);
            }
            else if (pEvent.EventTargetType == "Transcript")
            {
                this.arrTranscriptEvents.push(pEvent);
            }
        }
    }
    
    // PPT
    if (this.arrPPTEvents.length > 0)
    {
        // We use the same object view for all PPT events, so just use ID "PowerPoint"
        this.arrObjectViews.push({         ViewID:  "PowerPoint",
                                            Title:  "Slides", 
                                             Type:  "Image", 
                                    EventTargetID:  this.arrPPTEvents[0].EventTargetID,
                                       Timestamps:  this.arrPPTEvents, 
                                       SessionPID:  this.sessionPID });
    }

    // Streams
    var pStreams = SelectSingleNode(pXml, "Streams");
    var pArrStreams = SelectNodes(pStreams, "SimpleStream");
    
    // Streams: Archival segments
    var pXMLSegments = null;
    for (var i = 0; i < pArrStreams.length; i++)
    {
        var strStreamType = SelectSingleNodeValue(pArrStreams[i], "StreamType");
        if (strStreamType == "Archival")
        {
            var pSegments = SelectSingleNode(pArrStreams[i], "Segments");
            if (pSegments)
            {
                pXMLSegments = SelectNodes(pSegments, "Segment");
            }
        }
    }
    
    // Streams: Adjusted stream data
    for (var i = 0; i < pArrStreams.length; i++)
    {
        var strStreamType = SelectSingleNodeValue(pArrStreams[i], "StreamType");
        var strStreamUrl = SelectSingleNodeValue(pArrStreams[i], "StreamUrl");
        var strStreamPublicID = SelectSingleNodeValue(pArrStreams[i], "PublicID");
        var bBroadcast = SelectSingleNodeValue(pArrStreams[i], "IsBroadcast", "bool");
        var fAbsoluteStart = parseFloat(SelectSingleNodeValue(pArrStreams[i], "AbsoluteStart", "float"));
        var fAbsoluteEnd = parseFloat(SelectSingleNodeValue(pArrStreams[i], "AbsoluteEnd", "float"));
        var fRelativeStart = parseFloat(SelectSingleNodeValue(pArrStreams[i], "RelativeStart", "float"));
        var fRelativeEnd = parseFloat(SelectSingleNodeValue(pArrStreams[i], "RelativeEnd", "float"));
        var strStreamTag = SelectSingleNodeValue(pArrStreams[i], "Tag");
        
        if(strStreamType == "Archival")
        {
            this.archivalStream = {    Url: strStreamUrl,
                                    Length: fRelativeEnd,
                                       Tag: strStreamTag };
        }
        else if (strStreamType == "Streaming")
        {
            var pArrSegments = null;
            if (pSegments)
            {
                var segRelativeStart = 0;
                pArrSegments = new Array();
                for (var s = 0; s < pXMLSegments.length; s++)
                {
                    var segStart = SelectSingleNodeValue(pXMLSegments[s], "Start", "float");
                    var segEnd = SelectSingleNodeValue(pXMLSegments[s], "End", "float");
                    var segLength = segEnd - segStart;
                    pArrSegments.push({ RelativeStart: segRelativeStart,
                                               Offset: segStart - fRelativeStart });
                    segRelativeStart += segLength;
                }
            }
            
            var tagLookup = { SCREEN: "Screen Capture",
                              OBJECT: "Object Video" };
                              
            var strStreamDisplayName = "Other";
            if (tagLookup[strStreamTag])
            {
                strStreamDisplayName = tagLookup[strStreamTag];
            }

            this.arrObjectViews.push({   ViewID: strStreamPublicID,
                                          Title: strStreamDisplayName,
                                           Type: "Object",
                                            Url: strStreamUrl,
                                    IsBroadcast: bBroadcast,      
                                  AbsoluteStart: fAbsoluteStart,
                                    AbsoluteEnd: fAbsoluteEnd,
                                  RelativeStart: fRelativeStart,
                                    RelativeEnd: fRelativeEnd,
                                       Segments: pArrSegments,
                                       StreamID: strStreamPublicID });
        }
    }

    // Streams: Prune streams that don't overlap the delivery
    if (!this.isLive)
    {
        // prune object streams that fall outside of the dv video
        var i = 0;
        while (i < this.arrObjectViews.length)
        {
            var pView = this.arrObjectViews[i];

            if ((pView.Type == "Object") &&
                ((pView.RelativeEnd <= 0) || (pView.RelativeStart >= this.archivalStream.Length)))
            {
                this.arrObjectViews.splice(i, 1);
            }
            else
            {
                i++;
            }
        }
    }

    // Event Targets (PDF only for now)
    var pEventTargetsNode = SelectSingleNode(pXml, "EventTargets");
    var arrEventTargetNodes = SelectNodes(pEventTargetsNode, "SimpleEventTarget");
    for (var i = 0; i < arrEventTargetNodes.length; i++)
    {
        var title = "PDF" + (i > 0 ? i + 1 : "");
        var url = SelectSingleNodeValue(arrEventTargetNodes[i], "URL");
        var publicID = SelectSingleNodeValue(arrEventTargetNodes[i], "PublicID");

        //BUGBUG: Use title for type to create separate players for each PDF (so view state is not clobbered when switching tabs).
        this.arrObjectViews.push({ ViewID: publicID,
                                    Title: title,
                                     Type: "PDF",
                                      URL: url });
    }
}


function ThumbnailImage(pParent, url, time, objectViewID)
{
    this.Time = time;
    // The viewID of the object pane to switch to when clicked
    this.ObjectViewID = objectViewID;

    var m_imgThumb = CreateChildElement(pParent, "img", "thumbImage");
    m_imgThumb.src = url;
    m_imgThumb.alt = FormatRelativeTime(time);

    var m_bSelected = false;
    var m_bHovering = false;
    m_imgThumb.onmouseover = function(e)
    {
        e = GetEvent(e);
        m_bHovering = true;
        SetStyle();
        return false;
    }

    m_imgThumb.onmouseout = function(e)
    {
        e = GetEvent(e);
        m_bHovering = false;
        SetStyle();
        return false;
    }

    function SetStyle()
    {
        if (m_bSelected)
        {
            m_imgThumb.className = "thumbImageSelected";
        }
        else if (m_bHovering)
        {
            m_imgThumb.className = "thumbImageHover";
        }
        else
        {
            m_imgThumb.className = "thumbImage";
        }
    }

    this.SetSelected = function(bSelected)
    {
        m_bSelected = bSelected;
        SetStyle();
    }

    this.GetImage = function()
    {
        return m_imgThumb;
    }
}


// the thumbnail strip control
function Thumbnails(el, pViewer)
{
    var m_el = el;
    var m_pViewer = pViewer;
    var m_pCanvas = CreateChildElement(m_el, "div", "thumbnailContainer");

    var m_pSelectedItem = null;
    var m_arrItems = new Array();
    
    this.SetVisible = function(bVisible)
    {
        SetVisible(m_el, bVisible);
    }

    this.RenderContents = function(arrTimestamps, strSessionPID)
    {
        m_pCanvas.style.width = arrTimestamps.length * g_dThumbnailWidth + "px";

        for (var i = 0; i < arrTimestamps.length; i++)
        {
            var pEvent = arrTimestamps[i];

            var url = g_urlThumb + "?eventTargetPID=" + pEvent.EventTargetPublicID + "&sessionPID=" + strSessionPID + "&number=" + pEvent.SequenceNumber;
            var objectViewID = (pEvent.EventTargetType == "PowerPoint") ? "PowerPoint" : pEvent.StreamID;

            var imgThumb = new ThumbnailImage(m_pCanvas, url, pEvent.Time, objectViewID);

            function thumbnailClick(pItem)
            {
                SelectThumb(pItem);

                m_pViewer.SetVideoPosition(pItem.Time);

                if (pItem.ObjectViewID != "PowerPoint")
                {
                    m_pViewer.SelectObjectView(pItem.ObjectViewID);
                }
            }

            var image = imgThumb.GetImage();
            image.onclick = Clicker(image, thumbnailClick, imgThumb);

            m_arrItems.push(imgThumb);
        }
    }

    this.SelectThumbByIndex = function(iThumb)
    {
        if (iThumb < m_arrItems.length)
        {
            var thumb = m_arrItems[iThumb];

            SelectThumb(thumb);
        }
    }

    function SelectThumb(pItem)
    {
        if (m_pSelectedItem == pItem)
        {
            return;
        }
        
        if (m_pSelectedItem)
        {
            m_pSelectedItem.SetSelected(false);
        }
        
        m_pSelectedItem = pItem;
        m_pSelectedItem.SetSelected(true);
        
        ScrollIntoView(pItem);
    }

    function ScrollIntoView(pThumb)
    {
        var imageWidth = pThumb.GetImage().offsetWidth;
        var imageLeft = pThumb.GetImage().offsetLeft;
        var scrollWidth = m_el.offsetWidth;

        // center the image in the thumbnail client region
        var scroll = Math.max(0, (scrollWidth - imageWidth) / 2);

        m_el.scrollLeft = imageLeft - scroll;
    }

    this.SetWidth = function(width)
    {
        m_el.style.width = width + "px";
    }
}


function ParseEvent(pXML)
{
    var pEvent = {                Time: SelectSingleNodeValue(pXML, "Time", "float"),
                         EventTargetID: SelectSingleNodeValue(pXML, "ObjectIdentifier"),
                   EventTargetPublicID: SelectSingleNodeValue(pXML, "ObjectPublicIdentifier"),
                       EventTargetType: SelectSingleNodeValue(pXML, "EventTargetType"),
                        SequenceNumber: SelectSingleNodeValue(pXML, "ObjectSequenceNumber"),
                              StreamID: SelectSingleNodeValue(pXML, "ObjectStreamID"),
                               Caption: SelectSingleNodeValue(pXML, "Caption"),
                               EventID: SelectSingleNodeValue(pXML, "ID"),
                                  Data: SelectSingleNodeValue(pXML, "Data"),
                              UserName: SelectSingleNodeValue(pXML, "UserName")
    };

    if (!pEvent.Caption && pEvent.Data)
    {
        pEvent.Caption = pEvent.Data;
    }

    return pEvent;
}


function GetCurrentItem(arrItems, time)
{
    var iItem = 0;
    
    while ((iItem < arrItems.length) && (arrItems[iItem].Time <= time)) iItem++;
    
    if (iItem > 0) iItem--;

    return iItem;
}


// event handler helpers
// creates object with callback and payload used for handling events
function Clicker(pEl, pFunc, pPayload)
{
    pEl.clicker = { fn: pFunc, data: pPayload };

    function callback(e)
    {
        e = GetEvent(e);

        var pSrc = GetSrcElement(e);

        while (pSrc && !pSrc.clicker)
        {
            pSrc = GetParentElement(pSrc);
        }

        if (pSrc)
        {
            pSrc.clicker.fn(pSrc.clicker.data);
        }

        return false;
    }

    return callback;
}

function GetEvent(e)
{
    if (g_bIsIE)
    {
        event.cancelBubble = true;
        return event;
    }
    else
    {
        return e;
    }
}

function GetKey(e)
{
    if (e.keyCode)
    {
        return e.keyCode;
    }
    else
    {
        return e.charCode;
    }
}


// dom element helpers
function CreateElement(type, className, id)
{
    var el = document.createElement(type);
    if (id)
    {
        el.id = id;
    }
    if (className)
    {
        el.className = className;
    }
    return el;
}

function CreateChildElement(parent, type, className, id)
{
    var el = CreateElement(type, className);
    parent.appendChild(el);
    if (id)
    {
        el.id = id;
    }
    return el;
}

// Append text to element, escaping HTML
function AppendText(element, text)
{
    element.appendChild(document.createTextNode(text));
}

// Set text content of element, escaping HTML.
function SetText(element, text)
{
    while (element.hasChildNodes())
    {
        element.removeChild(element.firstChild);
    }

    AppendText(element, text);
}

// Set the text content of the element.
// Replace newlines with <br>s and HTML escape everything else.
function SetTextWithNewlineTranslation(element, text)
{
    SetTextWithReplacement(element, text, /\n/, function() { return CreateElement("BR"); });
}

// Set the text content of the element.
// Turn URLs into links, and HTML escape everything else.
function SetTextWithLinks(element, text)
{
    // Match http:// and https:// URLs
    var linkMatcher = new RegExp("\\bhttps?://\\S*", "i");

    // Given a URL match, build the link element and return it for insertion into parent element.
    function replacementDelegate(match)
    {
        var linkText = match[0];

        var linkNode = document.createElement("A");
        linkNode.href = linkText;
        linkNode.target = "_blank";

        // Set a click handler so we can cancel event bubbling to parent nodes.
        linkNode.onclick = function(e)
        {
            if (!e) e = GetEvent(e);
            if (e.stopPropagation) e.stopPropagation();
        }

        SetText(linkNode, linkText);

        return linkNode;
    }

    SetTextWithReplacement(element, text, linkMatcher, replacementDelegate);
}

// Set the text content of the element.
// Replace matches of regExp with element returned by replacementDelegate(match).
// HTML escapes everything else.
function SetTextWithReplacement(element, text, regExp, replacementDelegate)
{
    // Remove any existing children.
    while (element.hasChildNodes())
    {
        element.removeChild(element.firstChild);
    }

    var match;
    while (match = regExp.exec(text))
    {
        // Append any text before the match to the note
        if (RegExp.leftContext)
        {
            AppendText(element, RegExp.leftContext);
        }

        var replacementElement = replacementDelegate(match);

        element.appendChild(replacementElement);

        // Continue matching the remaining text after the match
        text = RegExp.rightContext;
    }

    // Add node for any text remaining after last match.
    if (text)
    {
        AppendText(element, text);
    }
}

function SetChild(element, childElement)
{
    while (element.hasChildNodes())
    {
        element.removeChild(element.firstChild);
    }

    element.appendChild(childElement);
}

function GetWindowHeight()
{
    if (g_bIsSafari || !g_bIsMozilla)
    {
        return document.documentElement.clientHeight;
    }
    else
    {
        return window.innerHeight;
    }
}

function GetWindowWidth()
{
    if (g_bIsSafari || !g_bIsMozilla)
    {
        return document.documentElement.clientWidth;
    }
    else
    {
        return window.innerWidth;
    }
}

function SetVisible(el, bVisible)
{
    if (el)
    {
        // actually changes layout rather than visibility
        if (bVisible)
        {
            el.style.display = "block";
        }
        else
        {
            el.style.display = "none";
        }
    }
}

function GetSrcElement(e)
{
    if (g_bIsMozilla)
    {
        return e.target;
    }
    else
    {
        return e.srcElement;
    }
}


function GetParentElement(pEl)
{
    if (g_bIsMozilla)
    {
        return pEl.parentNode;
    }
    else
    {
        return pEl.parentElement;
    }
}


// browser/silverlight detect helpers
var g_bIsIE = false;
var g_bIsIE6 = false;
var g_bIsMozilla = false;
var g_bIsSafari = false;
var g_bIsMac = false;
var g_bUsingSilverlight = false;
var g_sSilverlightVersion = "2.0.31005.0";

var sAgent = navigator.userAgent.toLowerCase();
if (sAgent.indexOf("msie") != -1)
{
    g_bIsIE = true;
    if (sAgent.indexOf("msie 6") != -1) g_bIsIE6 = true;
}
else if (sAgent.indexOf("mozilla") != -1) g_bIsMozilla = true;
if (sAgent.indexOf("safari") != -1) g_bIsSafari = true;
if (sAgent.indexOf("mac os x") != -1) g_bIsMac = true;


function FormatRelativeTime(dSeconds)
{
    // hooray, javascript
    function pad(int)
    {
        return int = (int < 10) ? '0' + int : int;
    }

    // surely there is some built-in way to do this
    var iHours = Math.floor(dSeconds / 3600);
    var iMinutes = Math.floor((dSeconds / 60) % 60);
    var iSeconds = Math.floor(dSeconds % 60);

    if (iHours > 0)
    {
        return iHours + ":" + pad(iMinutes) + ":" + pad(iSeconds);
    }
    else
    {
        return iMinutes + ":" + pad(iSeconds);
    }
}

function FormatFileTime(lSeconds)
{
    // convert time from windows file time epoch to unix timestamp epoch.  convert from seconds to millis.
    var javascriptTime = (lSeconds - 11644473600) * 1000;

    var pTime = new Date();
    pTime.setTime(javascriptTime);
    
    var h = pTime.getHours();
    var m = pTime.getMinutes();
    var s = pTime.getSeconds();
    var period = (h < 12 ? "am" : "pm");
    
    if( h > 12 )
    {
        h -= 12;
    }
    
    function padTime(i, c)
    {
        if (i < 10)
        {
            i = c + i;
        }
        return i;
    }

    h = padTime(h, "<span style='visibility:hidden'>0</span>");
    m = padTime(m, "0");
    s = padTime(s, "0");

    return h + ":" + m + ":" + s + " " + period;
}

// XMLHttpRequest support methods

// Wikipedia.org: Here is a patch allowing direct invocation of XMLHttpRequest in browsers that don't supply one directly:
// BUGBUG:  untested.
if (!window.XMLHttpRequest) XMLHttpRequest = function()
{
    try { return new ActiveXObject("MSXML3.XMLHTTP") } catch (e) { }
    try { return new ActiveXObject("MSXML2.XMLHTTP.3.0") } catch (e) { }
    try { return new ActiveXObject("Msxml2.XMLHTTP") } catch (e) { }
    try { return new ActiveXObject("Microsoft.XMLHTTP") } catch (e) { }
    throw new Error("Could not find an XMLHttpRequest alternative.")
};

// execute an XMLHttpRequest using the specified callback function (after checking response validity)
// the properties of the params object form the name/value pairs to be passed as parameters
// e.g. params = { queryParam1: "value", queryParam2: "value2" };
function CreateRequest(url, params, fnCallback)
{
    var request = new XMLHttpRequest();

    request.open("POST", url, true);
    request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

    request.onreadystatechange = function()
    {
        if (request.readyState == 4 && fnCallback != null)
        {
            if (request.status == 200 && request.responseXML)
            {
                fnCallback(request.responseXML.documentElement, true);
            }
            else
            {
                fnCallback(null, false);
            }
        }
    };

    var paramArray = new Array();
    for (var param in params)
    {
        if (params[param])
        {
            paramArray.push(param + "=" + params[param]);
        }
    }

    var strVars = paramArray.join("&");

    request.send(strVars);
}


// xml parsing helper functions
function SelectNodes(pParent, sTag)
{
    if (g_bIsMozilla)
    {
        var pEvaluator = new XPathEvaluator();
        var pResult = pEvaluator.evaluate(sTag, pParent, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);

        var arrNodes = new Array();

        if (pResult != null)
        {
            var pElement = pResult.iterateNext();
            while (pElement)
            {
                arrNodes.push(pElement);
                pElement = pResult.iterateNext();
            }
        }

        return arrNodes;
    }
    else
    {
        return pParent.selectNodes(sTag);
    }
}

function SelectSingleNode(pParent, sTag)
{
    if (g_bIsMozilla)
    {
        var pEvaluator = new XPathEvaluator();
        var pResult = pEvaluator.evaluate(sTag, pParent, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);

        if (pResult != null)
        {
            return pResult.singleNodeValue;
        }
        else
        {
            return null;
        }
    }
    else
    {
        return pParent.selectSingleNode(sTag);
    }
}

function SelectSingleNodeValue(pParent, sTag, type)
{
    var pNode = SelectSingleNode(pParent, sTag);
    if (!pNode)
    {
        return null;
    }

    var value;
    if (g_bIsIE)
    {
        value = pNode.text;
    }
    else if (g_bIsMozilla)
    {
        value = pNode.textContent;
    }

    if (type == "float")
    {
        return parseFloat(value);
    }
    else if (type == "int")
    {
        return parseInt(value);
    }
    else if (type == "bool")
    {
        return (value == "true");
    }
    else
    {
        return value;
    }
}

function getCookie(c_name)
{
    if (document.cookie.length > 0)
    {
        c_start = document.cookie.indexOf(c_name + "=");
        if (c_start != -1)
        {
            c_start = c_start + c_name.length + 1;
            c_end = document.cookie.indexOf(";", c_start);
            if (c_end == -1)
            {
                c_end = document.cookie.length;
            }
            return unescape(document.cookie.substring(c_start, c_end));
        }
    }
    return "";
}

function setCookie(c_name, value, expiredays)
{
    var exdate = new Date();
    exdate.setDate(exdate.getDate() + expiredays);
    document.cookie = c_name + "=" + escape(value) + ((expiredays == null) ? "" : ";expires=" + exdate.toGMTString());
}

function deleteCookie(c_name)
{
    document.cookie = c_name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT;';
}
