var g_dBroadcastLatencyEstimate = 13.0;

var g_cPaneDefaultTopPadding = 10;
var g_cPaneContentDividerWidth = 1;

var g_cInfoPanePadding = 10;

var g_cEventViewerMinHeight = 20;

var g_cNotesInputHeight = 94;
var g_cNotesChooseUserHeight = 29;
var g_cNotesChooseChannelHeight = 23;

var g_cSearchChromeHeight = 78;

var g_cQuestionEntryHeight = 30;

// tab control that hosts multiple event viewers
function EventTabViewer(el, pViewer)
{
    var m_el = el;
    var m_pViewer = pViewer;
    
    var m_pLastStatus = null;
    var m_pSelectedView = null;
    
    // create our tab control (boolean specifies small tabs)
    var m_pTabControl = new TabControl(document.getElementById( "eventViewerBar" ), OnSelectView, false);
    
    // create the info tab
    var m_pInfo = new InfoTab(document.getElementById("infoDiv"), m_pViewer);
    m_pInfo.Title = "Info";
    m_pInfo.ViewID = "Info";
    m_pInfo.SetVisible(false);
    
    // create our table of contents
    var m_pContents = new TocViewer(document.getElementById("tocDiv"), m_pViewer);
    m_pContents.Title = "Contents";
    m_pContents.ViewID = "Contents";
    m_pContents.SetVisible(false);
    
    // create our transcript viewer
    var m_pTranscript = new TocViewer(document.getElementById("transDiv"), m_pViewer);
    m_pTranscript.Title = "Captions";
    m_pTranscript.ViewID = "Captions";
    m_pTranscript.SetVisible(false);
    
    // create our notes viewer (current disabled)
    var m_pNotes = new NotesViewer(document.getElementById("notesDiv"), m_pViewer);
    m_pNotes.Title = "Notes";
    m_pNotes.ViewID = "Notes";
    m_pNotes.SetVisible(false);
    
    // create our search tab
    var m_pSearch = new SearchViewer(document.getElementById("searchDiv"), m_pViewer);
    m_pSearch.Title = "Search";
    m_pSearch.ViewID = "Search";
    m_pSearch.SetVisible(false);

    this.RenderContents = function(arrEvents, arrTranscriptEvents)
    {
        // initialize our tab control with our array of viewers

        // Contents
        if (arrEvents.length > 0)
        {
            m_pTabControl.AddView(m_pContents);
            m_pContents.RenderContents(arrEvents);
        }

        // Transcript
        if (arrTranscriptEvents.length > 0)
        {
            m_pTabControl.AddView(m_pTranscript);
            m_pTranscript.RenderContents(arrTranscriptEvents);
        }
        else
        {
            document.getElementById("searchTranscriptRadio").style.display = "none";
            document.getElementById("searchTranscriptLabel").style.display = "none";
        }

        // Notes
        m_pTabControl.AddView(m_pNotes);
        m_pNotes.RenderContents();

        // Search
        if (!m_pViewer.isLive)
        {
            m_pTabControl.AddView(m_pSearch);
        }

        // Info
        m_pTabControl.AddView(m_pInfo);

        // Hack to accomodate 5 event viewer tabs.
        if ((arrEvents.length > 0) && (arrTranscriptEvents.length > 0) && !m_pViewer.isLive)
        {
            g_dMinEventViewerWidth = 415;
            g_dMinViewerWidth = 1122;
        }

        m_pTabControl.UpdateCurrentView();
    }

    this.UpdateStatus = function UpdateStatus(status)
    {
        m_pLastStatus = status;
        m_pSelectedView.UpdateStatus(status);
    }

    // callback for clicking on tabs
    function OnSelectView(pView)
    {
        // If we're selecting a new tab, hide the old one
        if (m_pSelectedView && (m_pSelectedView != pView))
        {
            m_pSelectedView.SetVisible(false);
        }

        m_pSelectedView = pView;

        if (m_pLastStatus)
        {
            m_pSelectedView.UpdateStatus(m_pLastStatus);
        }
        
        m_pSelectedView.SetVisible(true);
        m_pSelectedView.SetFocus();
    }

    this.SetHeight = function SetHeight(controlHeight)
    {
        var eventViewerHeight = controlHeight - g_dContainerSpacing;
        m_el.style.height = controlHeight - g_dContainerSpacing + "px";

        // Client panes get eventViewerHeight minus tab bar height
        m_pInfo.OnResize(eventViewerHeight - g_dTabBarOffsetHeight);
        m_pContents.OnResize(eventViewerHeight - g_dTabBarOffsetHeight);
        m_pTranscript.OnResize(eventViewerHeight - g_dTabBarOffsetHeight);
        m_pSearch.OnResize(eventViewerHeight - g_dTabBarOffsetHeight);
        if (!m_pNotes.Disabled)
        {
            m_pNotes.OnResize(eventViewerHeight - g_dTabBarOffsetHeight);
        }
    }
    
    this.SetWidth = function SetWidth(controlWidth)
    {
        m_el.style.width = controlWidth + "px";
    }
}

// Info tab (presenter name, bio, etc.)
function InfoTab(el, pViewer)
{
    var m_el = el;
    var m_elInfoContents = document.getElementById("infoContents");
    var m_elSpeakerBios = document.getElementById("speakerBios");

    var contributors = pViewer.contributors;

    for (var i = 0; i < contributors.length;  i++)
    {
        var displayName = contributors[i].DisplayName;
        // BUGBUG: Convert consecutive spaces to &nbsp; ?
        var bio = contributors[i].Bio;

        CreateChildElement(m_elSpeakerBios, "HR");

        var table = CreateChildElement(m_elSpeakerBios, "TABLE", "info");
        var row = table.insertRow(-1);

        var labelCell = row.insertCell(-1);
        labelCell.className = "label";
        SetText(labelCell, "Presenter:");

        var speakerCell = row.insertCell(-1);
        SetText(speakerCell, displayName);

        var bioText = CreateChildElement(m_elSpeakerBios, "DIV", "contentText");
        SetTextWithNewlineTranslation(bioText, bio);
    }

    this.RenderContents = function() { }
    
    this.UpdateStatus = function(status) { }
    
    this.SetVisible = function(bVisible)
    {
        SetVisible(m_el, bVisible);
    }
    
    this.SetFocus = function() { }

    this.OnResize = function OnResize(controlHeight)
    {
        var paneHeight = controlHeight - g_cPaneDefaultTopPadding;
        m_el.style.height = paneHeight + "px";
        m_elInfoContents.style.height = paneHeight - (g_cInfoPanePadding * 2) - g_cPaneContentDividerWidth + "px";
    }
}

// visual representation for events - pass in the parent, event object, and options
// options:
//  DeleteCallback - when present a delete button is shown for the visual and when clicked
//                   calls the passed in function with the event
function EventVisual(elParent, pEvent, pViewer, pOptions)
{
    var m_pEvent = pEvent;

    var m_pViewer = pViewer;

    var m_bEditing = false;
    var m_bSelected = false;
    var m_bHovering = false;

    var m_elContainer = CreateChildElement(elParent, "div", "eventVisual");

    var innerContainer = CreateChildElement(m_elContainer, "div", "");

    var divTime = CreateChildElement(innerContainer, "div", "eventVisualTime");
    if (m_pViewer.isLive)
    {
        divTime.innerHTML = FormatFileTime(m_pEvent.Time);
    }
    else
    {
        divTime.innerHTML = FormatRelativeTime(m_pEvent.Time);
    }

    if (pOptions && pOptions.ShowUsernames)
    {
        var divUser = CreateChildElement(innerContainer, "div", "eventVisualUser");
        if (m_pEvent.UserName)
        {
            divUser.innerHTML = "[" + m_pEvent.UserName + "]";
        }
        else
        {
            var spanAnonymousUser = CreateChildElement(divUser, "span", "eventVisualAnonymousUser");
            spanAnonymousUser.innerHTML = "[anonymous]"
        }
    }

    var divCaption = CreateChildElement(innerContainer, "div", "eventVisualElement");
    if (!m_pEvent.Caption)
    {
        divCaption.innerHTML = "";
    }
    else
    {
        // Create text node to escape HTML in text entry
        SetTextWithLinks(divCaption, m_pEvent.Caption);
    }

    // IE6 highlighting breaks without position: relative
    if (g_bIsIE6)
    {
        m_elContainer.style.position = "relative";
    }

    var m_elActionPanel = null;
    var m_elEditLink = null;
    var m_elCancelEditLink = null;
    var m_elDeleteLink = null;
    // Only instantiate note action panel if we're dealing with notes.
    // Presence of DeleteCallback is effectively a flag that we're dealing with notes.
    if (pOptions && pOptions.DeleteCallback && (!m_pEvent.UserName || (m_pEvent.UserName == m_pViewer.userName)))
    {
        // position: relative impacts perf, only use when necessary.
        m_elContainer.style.position = "relative";

        m_elActionPanel = CreateChildElement(m_elContainer, "div", "noteActionPanel");
        
        m_elEditLink = CreateChildElement(m_elActionPanel, "a", "editLink");
        m_elEditLink.href = "#";
        m_elEditLink.innerHTML = "edit";
        m_elEditLink.onclick = new Clicker(m_elEditLink, pOptions.EditCallback, pEvent);

        m_elCancelEditLink = CreateChildElement(m_elActionPanel, "a", "cancelEditLink");
        m_elCancelEditLink.href = "#";
        m_elCancelEditLink.innerHTML = "cancel edit";
        m_elCancelEditLink.onclick = new Clicker(m_elCancelEditLink, pOptions.CancelEditCallback);

        m_elDeleteLink = CreateChildElement(m_elActionPanel, "a", "deleteLink");
        m_elDeleteLink.href = "#";
        m_elDeleteLink.innerHTML = "delete";
        m_elDeleteLink.onclick = new Clicker(m_elDeleteLink, pOptions.DeleteCallback, pEvent);
    }

    m_elContainer.onmouseover = function(e)
    {
        e = GetEvent(e);

        if (m_elActionPanel)
        {
            m_elActionPanel.style.visibility = "visible";
        }

        m_bHovering = true;
        SetStyle();
        return false;
    }

    m_elContainer.onmouseout = function(e)
    {
        e = GetEvent(e);

        if (m_elActionPanel)
        {
            m_elActionPanel.style.visibility = "hidden";
        }

        m_bHovering = false;
        SetStyle();
        return false;
    }


    function SetStyle()
    {
        if (m_bEditing)
        {
            m_elContainer.className = "eventVisualEditing";

            // If note action panel is displayed, show "cancel edit" link.
            if (m_elActionPanel)
            {
                m_elCancelEditLink.style.display = "inline";
                m_elEditLink.style.display = "none";
                m_elDeleteLink.style.display = "none";

                // We're changing padding to compensate for border, so tweak alignment to maintain position
                m_elActionPanel.style.top = "1px";
                m_elActionPanel.style.right = "1px";
            }
        }
        else
        {
            if (m_bSelected)
            {
                m_elContainer.className = "eventVisualSelected";
            }
            else if (m_bHovering)
            {
                m_elContainer.className = "eventVisualHover";
            }
            else
            {
                m_elContainer.className = "eventVisual";
            }

            // If note action panel is displayed, show "edit" and "delete" links.
            if (m_elActionPanel)
            {
                m_elCancelEditLink.style.display = "none";
                m_elEditLink.style.display = "inline";
                m_elDeleteLink.style.display = "inline";

                // We're changing padding to compensate for border, so tweak alignment to maintain position
                m_elActionPanel.style.top = "2px";
                m_elActionPanel.style.right = "2px";
            }
        }
    }

    this.SetSelected = function(bSelected, bEditing)
    {
        m_bSelected = bSelected;
        m_bEditing = bEditing;
        SetStyle();
    }

    this.GetContainer = function()
    {
        return m_elContainer;
    }
}

// general viewer for events
function EventViewer(el, pViewer, pOptions)
{
    var m_el = el;
    var m_pViewer = pViewer;

    var m_divContents = CreateChildElement(m_el, "div");
    var m_divScrollPane = null;
    var m_divToggleThumbs = null;

    var m_pSelectedItem = null;
    // Remember the current edit mode to avoid unnecessary style updating (causes hitching in IE8).
    var m_bSelectedItemEditing = false;
    
    var m_arrTimestamps = null;
    var m_arrVisuals = new Array();
    
    var m_bInitialized = false;

    this.RenderContents = function(arrTimestamps)
    {
        // this implies that we will only view notes sorted by time                   
        arrTimestamps.sort(function(a, b) { return a.Time - b.Time });

        m_divContents.innerHTML = "";
        m_arrTimestamps = arrTimestamps;

        // clear existing visuals
        m_arrVisuals.splice(0, m_arrVisuals.length);

        // if we don't have any elements, hide the list
        if (m_arrTimestamps.length == 0)
        {
            SetVisible(m_divContents, false);
            return;
        }
        SetVisible(m_divContents, true);

        // create event visuals for our timestamps
        for (var i = 0; i < arrTimestamps.length; i++)
        {
            var pEventVisual = new EventVisual(m_divContents, arrTimestamps[i], m_pViewer, pOptions);
            m_arrVisuals.push(pEventVisual);

            var container = pEventVisual.GetContainer();

            if (!m_pViewer.isLive)
            {
                function eventClick(pItem)
                {
                    m_pViewer.SetVideoPosition(pItem.Time);
                    
                    if(pItem.EventTargetType != "PowerPoint")
                    {
                        m_pViewer.SelectObjectView(pItem.StreamID);
                    }
                }

                container.onclick = Clicker(container, eventClick, arrTimestamps[i]);
            }
            else
            {
                //BUGBUG: Should use CSS
                container.style.cursor = "normal";
            }
        }
    }

    // Highlight event and scroll into view.  If editing, indicate with different styling.
    function SelectItem(pItemToSelect, bEditing)
    {
        if (pItemToSelect == m_pSelectedItem)
        {
            // If we're toggling edit mode on/off for the currently selected event, we need to update styles.
            if (bEditing != m_bSelectedItemEditing)
            {
                m_pSelectedItem.SetSelected(true, bEditing);
                m_bSelectedItemEditing = bEditing;
            }
            
            return;
        }

        if (m_pSelectedItem)
        {
            m_pSelectedItem.SetSelected(false);
        }

        m_pSelectedItem = pItemToSelect;
        // Update cached value of edit mode to reflect whether we're editing the new event.
        m_bSelectedItemEditing = bEditing;

        ScrollIntoView(m_pSelectedItem);
        m_pSelectedItem.SetSelected(true, bEditing);
    }

    // Find event visual in parallel list of event.
    this.SelectEditEvent = function(pEditEvent)
    {
        for (var i = 0; i < m_arrTimestamps.length; i++)
        {
            var pEvent = m_arrTimestamps[i];
            if (pEvent.EventID == pEditEvent.EventID)
            {
                var eventVisual = m_arrVisuals[i];
                SelectItem(eventVisual, true);
            }
        }
    }

    this.UpdateStatus = function(status)
    {
        if (!m_arrTimestamps)
        {
            return;
        }

        var iItem = GetCurrentItem(m_arrTimestamps, status.Time);
        if (iItem < m_arrVisuals.length)
        {
            SelectItem(m_arrVisuals[iItem]);
        }
    }
    
    this.SetVisible = function(bVisible)
    {
        SetVisible(m_el, bVisible);
    }
    
    this.OnResize = function OnResize(controlHeight)
    {
        m_el.style.height = controlHeight + "px";
    }

    function ScrollIntoView( pVisual )
    {
        var visHeight = pVisual.GetContainer().offsetHeight;
        var visTop = pVisual.GetContainer().offsetTop;

        var scrollHeight = m_el.offsetHeight

        var scroll = Math.max(0, (scrollHeight - visHeight) / 2);

        // Guard against negative scrollTop for IE6
        m_el.scrollTop = Math.max(0, visTop - scroll);
    }
}

// Table of Contents (slide & thumbnail events)
function TocViewer(el, pViewer)
{
    var m_el = el;
    var m_pViewer = pViewer;

    var m_pEventViewer = new EventViewer(CreateChildElement(el, "div", "tocContents"), m_pViewer);

    this.RenderContents = function(arrTimestamps)
    {
        m_pEventViewer.RenderContents(arrTimestamps);
    }

    this.UpdateStatus = function(status)
    {
        m_pEventViewer.UpdateStatus(status);
    }

    this.SetVisible = function(bVisible)
    {
        SetVisible(m_el, bVisible);
    }

    this.SetFocus = function() { }

    this.OnResize = function OnResize(controlHeight)
    {
        var paneHeight = controlHeight - g_cPaneDefaultTopPadding;
        m_el.style.height = paneHeight + "px";
        m_pEventViewer.OnResize(paneHeight - g_cPaneContentDividerWidth);
    }
}

// viewer for taking and viewing notes
function NotesViewer(el, pViewer)
{
    var m_el = el;
    var m_pViewer = pViewer;
    var m_arrNotes = new Array();
    var m_noteStartTime = 0;

    // we have an event viewer - we probably should subclass it instead
    var m_pEventViewer = new EventViewer(document.getElementById("notesDivDisplay"), m_pViewer, { EditCallback:         EditNote,
                                                                                                  CancelEditCallback:   ExitEditMode,
                                                                                                  DeleteCallback:       DeleteNote,
                                                                                                  UserName:             m_pViewer.userName,
                                                                                                  ShowUsernames:        true });
    var m_pLastEvent = null;

    // The note currently being edited, if any.
    var m_pEditingEvent = null;
    
    // elements for choosing and displaying the user
    var m_userDiv = document.getElementById("userDiv");
    var m_notesUserSelect = document.getElementById("notesUserSelect");
    var m_myNotesOptionGroup = document.getElementById("myNotesOptionGroup");
    var m_publicNotesOptionGroup = document.getElementById("publicNotesOptionGroup");
    var m_publicNotesToggleDiv = document.getElementById("publicNotesToggleDiv");
    var m_publicNotesToggleCheckbox = document.getElementById("publicNotesToggleCheckbox");
    var m_publicNotesToggleLabel = document.getElementById("publicNotesToggleLabel");

    // elements for choosing and displaying the notes channel
    var m_channelDiv = document.getElementById("channelDiv");
    var m_channelName = document.getElementById("channelName");
    var m_leaveChannelSpan = document.getElementById("leaveChannelSpan");
    var m_channelInput = document.getElementById("channelInput");
    var m_changeChannelButton = document.getElementById("changeChannelButton");

    // Note entry area
    var m_notesInputDiv = document.getElementById("notesInputDiv");
    var m_elCancelEditLinkSpan = document.getElementById("cancelEditLinkSpan");
    var m_elCancelEditLink = document.getElementById("cancelEditLink");
    m_elCancelEditLink.onclick = Clicker(m_elCancelEditLink, ExitEditMode);    
    var m_elNotesInputMessage = document.getElementById("notesInputMessage");
    // Notes input textarea
    var m_notesText = document.getElementById("inputTextArea");
    m_notesText.onkeypress = NotesKeyPressed;

    m_notesUserSelect.onchange = function(e)
    {
        if (m_notesUserSelect.value == "#login")
        {
            // Use replace so we don't create a useless history state when logging in.
            location.replace(pViewer.loginURL);
        }
        else
        {
            pViewer.notesUser = m_notesUserSelect.value;

            RenderControls();
            m_pViewer.Resize();

            // If we have enough info to specify a notes set, retrieve notes from server.
            if (m_pViewer.userName || m_pViewer.notesUser || m_pViewer.channel)
            {
                PopulateNotesFromServer();
            }
            // Anonymous user with no channel set, just clear notes content window.
            else
            {
                m_arrNotes = new Array();
                m_pEventViewer.RenderContents(m_arrNotes);
            }

            SetFocus();
        }
    }

    m_publicNotesToggleCheckbox.onclick = function(e)
    {
        function onPublicToggled( pXML, bSuccess )
        {
            if(bSuccess && pXML)
            {
                // Page returns resulting value
                var retVal = SelectSingleNodeValue( pXML, "Public" );
                m_publicNotesToggleCheckbox.checked = (retVal == "true");
                m_publicNotesToggleLabel.className = (retVal == "true") ? "public" : "private";
            }
        }
        
        var params =
        {
            id:     m_pViewer.deliveryID,
            public: m_publicNotesToggleCheckbox.checked
        }
        
        CreateRequest(g_urlNoteTogglePublic, params, onPublicToggled);
    }

    m_leaveChannelSpan.onclick = function(e)
    {
        deleteCookie("Channel");

        m_pViewer.channel = "";
        m_channelInput.value = "";

        if (m_pViewer.userName)
        {
            m_changeChannelButton.value = "apply";
            ToggleChangeChannelControl();
        }
        else
        {
            // Anonymous users must be in a channel, so clear display and prompt for new channel
            m_arrNotes = new Array();
            m_pEventViewer.RenderContents(m_arrNotes);

            m_changeChannelButton.value = "change";
            ToggleChangeChannelControl();
        }
    }

    m_channelInput.onkeypress = function(e)
    {
        e = GetEvent( e );
        
        // return if the user hasn't hit enter
        if( GetKey(e)!= 13 )
        {
            return true;
        }
        
        ToggleChangeChannelControl();
        return false;
    }
    
    m_changeChannelButton.onclick = function(e)
    {
        ToggleChangeChannelControl();
        return false;
    }

    function ToggleChangeChannelControl()
    {
        if( m_changeChannelButton.value == "change" )
        {
            m_channelInput.value = (m_pViewer.channel ? m_pViewer.channel : "");
            SetVisible( m_channelInput, true );
            SetVisible( m_channelName, false );
            m_changeChannelButton.value = "apply";
        }
        else
        {
            if (!m_pViewer.userName && !m_channelInput.value)
            {
                alert("Please select a channel or log in to take notes.");
                m_channelInput.focus();
                return;
            }

            m_pViewer.channel = m_channelInput.value;

            RenderControls();
            
            setCookie("Channel", m_pViewer.channel, 365);
            if (m_pViewer.channel)
            {
                SetText(m_channelName, m_pViewer.channel);
            }
            else
            {
                var noChannelLabel = CreateElement("span", null, "noChannelLabel");
                SetText(noChannelLabel, "(no channel)");
                SetChild(m_channelName, noChannelLabel);
            }
            // workaround for Firefox bug causing cursor to disappear
            m_channelInput.blur();
            SetVisible( m_channelInput, false );
            SetVisible( m_channelName, true );
            m_changeChannelButton.value = "change";
            
            PopulateNotesFromServer();
        }

        SetFocus();
    }

    function PopulateNotesFromServer()
    {
        function onNotesXML(pXML, bSuccess)
        {
            var error = "true";
            if (pXML)
            {
                error = SelectSingleNodeValue(pXML, "Error");
            }

            if (error == "false" && bSuccess)
            {
                // clear the current array
                m_arrNotes = new Array();
                var pEvents = SelectSingleNode(pXML, "Events");
                var pArrTimestamps = SelectNodes(pEvents, "SimpleTimestamp");

                // parse the xml
                for (var i = 0; i < pArrTimestamps.length; i++)
                {
                    m_arrNotes.push(ParseEvent(pArrTimestamps[i]));
                }

                // render
                m_pEventViewer.RenderContents(m_arrNotes);

                SetFocus();
            }
            else
            {
                // TODO: show message and disable note entry
            }
        }

        var params =
        {
            id:                 m_pViewer.deliveryID,
            type:               "notes",
            notesUser:          m_pViewer.notesUser,
            channelName:        encodeURIComponent(m_pViewer.channel),
            deliveryRelative:   (m_pViewer.isLive ? null : "true")
        }

        CreateRequest(g_urlSearchResults, params, onNotesXML);
    }

    function EditNote(pNote)
    {
        m_pEditingEvent = pNote;

        m_pEventViewer.SelectEditEvent(m_pEditingEvent);
    
        m_notesText.value = pNote.Caption;

        m_elNotesInputMessage.style.display = "none";
        m_elCancelEditLinkSpan.style.display = "inline";
        m_notesText.className = "editing";

        // setTimeout is a workaround for IE.  Fails to set focus if called synchronously.
        setTimeout(function() { SetFocus(); }, 0);

        return false;
    }

    function ExitEditMode()
    {
        m_pEditingEvent = null;

        // Clear notes text entry.
        m_notesText.value = "";
        
        m_elNotesInputMessage.style.display = "inline";
        m_elCancelEditLinkSpan.style.display = "none";
        m_notesText.className = "";

        SetFocus();
    }

    function DeleteNote(pToDelete)
    {
        ExitEditMode();
    
        // go through our list to find the note we need to delete
        for (var n in m_arrNotes)
        {
            if (pToDelete == m_arrNotes[n])
            {
                // take it out of our array and rerender the other notes
                m_arrNotes.splice(n, 1);
                m_pEventViewer.RenderContents(m_arrNotes);
            }
        }

        CreateRequest(g_urlNoteDelete, { eventid: pToDelete.EventID });
        return false;
    }

    function NotesKeyPressed(e)
    {
        e = GetEvent(e);

        // If user has hit [Enter], submit note.
        if (GetKey(e) == 13)
        {
            SubmitNote();

            // Prevent [Enter] from submitting form.
            return false;
        }

        // If user has hit [Esc], cancel edit or clear note entry
        if (GetKey(e) == 27)
        {
            if (m_pEditingEvent)
            {
                ExitEditMode();
            }
            else
            {
                m_notesText.value = "";
            }
        
            return false;
        }

        // Set the note start when the first character is typed in the notes area
        if (m_notesText.value.length == 0)
        {
            if (m_pViewer.isLive)
            {
                m_noteStartTime = new Date().getTime();
                
                // When viewing a live broadcast, offset the note by an estimated delay to account for
                // latency between video capture and display.
                if (!m_pViewer.isStandaloneNotes)
                {
                    m_noteStartTime -= g_dBroadcastLatencyEstimate;
                }
            }
            else
            {
                // BUGBUG: We should pull the exact video time instead of using the m_pLastEvent proxy.
                m_noteStartTime = m_pLastEvent ? m_pLastEvent.Time : 0;
            }
        }

        // Allow character to be entered
        return true;
    }

    function SubmitNote()
    {
        if (!m_pViewer.userName && !m_pViewer.channel)
        {
            alert("Please select a channel or log in to take notes.");
            return;
        }

        // Return if we have an empty note.
        var caption = m_notesText.value;
        if (caption == "")
        {
            return;
        }
        m_notesText.value = "";

        if (m_pEditingEvent)
        {
            UpdateNote(caption);
        }
        else
        {
            CreateNote(caption);
        }
    }

    // Change the text of the note being edited.
    function UpdateNote(caption)
    {
        // If there was an error updating the note, pop up an alert.
        function onNoteUpdated(pXML, bSuccess)
        {
            var errorMessage = SelectSingleNodeValue(pXML, "ErrorMessage");

            if (errorMessage)
            {
                alert(errorMessage);
            }
        }

        var params =
        {
            eventid:    m_pEditingEvent.EventID,
            data:       encodeURIComponent(caption)
        };

        CreateRequest(g_urlNoteUpdate, params, onNoteUpdated);

        // We update the relevant note immediately, and just notify the user via popup if the update failed.
        
        m_pEditingEvent.Caption = caption;

        m_pEventViewer.RenderContents(m_arrNotes);

        // Highlight updated note.
        m_pEventViewer.UpdateStatus(m_pEditingEvent);

        ExitEditMode();
    }

    function CreateNote(caption)
    {
        // For live note taking, get the time difference between note start and current time
        // to determine note start offset from server time.
        var noteStartTimeOffset = (m_noteStartTime - new Date().getTime()) / 1000;
        
        var pNote = { Time:           (m_pViewer.isLive ? noteStartTimeOffset : m_noteStartTime),
                      UserName:       m_pViewer.userName,
                      EventTargetID:  1,
                      SequenceNumber: m_arrNotes.length + 1,
                      Caption:        caption };

        function onNoteSubmitted(pXML, bSuccess)
        {
            var ret = SelectSingleNodeValue(pXML, "ReturnCode");
            pNote.EventID = SelectSingleNodeValue(pXML, "EventID");
            // If taking live notes, retrieve note time from server.
            if(m_pViewer.isLive)
            {
                pNote.Time = SelectSingleNodeValue(pXML, "Time", "float");
            }
            
            if((ret == "true") && bSuccess)
            {
                // Add the note to the notes array and re-render our event viewer.
                m_arrNotes.push(pNote);
                m_pEventViewer.RenderContents(m_arrNotes);
                // Use pNote.Time to highlight added note.
                m_pEventViewer.UpdateStatus(pNote);
            }
            else
            {
                // TODO: figure out how we want to handle errors
            }
        }

        var params =
        {
            deliveryid:         m_pViewer.deliveryID,
            time:               pNote.Time,
            data:               encodeURIComponent(pNote.Caption),
            channelName:        encodeURIComponent(m_pViewer.channel),
            deliveryRelative:   (m_pViewer.isLive ? null : "true")
        };
        
        CreateRequest( g_urlNoteSubmit, params, onNoteSubmitted );
    }

    this.SetVisible = function(bVisible)
    {
        SetVisible(m_el, bVisible);
    }

    this.OnResize = function(controlHeight)
    {
        m_el.style.height = controlHeight + "px";

        var chromeHeight = g_cNotesChooseUserHeight + g_cPaneContentDividerWidth;
        if (!pViewer.notesUser)
        {
            chromeHeight += g_cNotesChooseChannelHeight + g_cNotesInputHeight;
        }

        var eventViewerHeight = Math.max(g_cEventViewerMinHeight, controlHeight - chromeHeight);
        m_pEventViewer.OnResize(eventViewerHeight);
    }

    function RenderControls()
    {
        if (pViewer.notesUser)
        {
            m_publicNotesToggleDiv.style.display = "none";

            m_channelDiv.style.display = "none";
            m_notesInputDiv.style.display = "none";
        }
        else
        {
            if (pViewer.userName && !pViewer.channel && pViewer.bAllowPublishNotes)
            {
                m_publicNotesToggleDiv.style.display = "block";
            }
            else
            {
                m_publicNotesToggleDiv.style.display = "none";
            }

            m_channelDiv.style.display = "block";
            m_notesInputDiv.style.display = "block";
        }
    }

    function FillNotesUserSelect()
    {
        var myNotesOption = CreateChildElement(m_myNotesOptionGroup, "option");
        myNotesOption.innerHTML = (m_pViewer.userName ? m_pViewer.userName : "(anonymous)");
        myNotesOption.value = "";

        if (!m_pViewer.userName)
        {
            var loginOption = CreateChildElement(m_myNotesOptionGroup, "option");
            loginOption.innerHTML = "Log in...";
            loginOption.value = "#login";
        }

        for (var i in m_pViewer.publicNotesUsers)
        {
            // Don't show duplicate notes stream for current user.
            if (m_pViewer.userName == m_pViewer.publicNotesUsers[i]) continue;

            var publicUserOption = CreateChildElement(m_publicNotesOptionGroup, "option");
            publicUserOption.innerHTML = m_pViewer.publicNotesUsers[i];
            publicUserOption.value = m_pViewer.publicNotesUsers[i];
        }
    }

    function SetFocus()
    {
        if(m_el.style.display != "none")
        {
            m_notesUserSelect.style.display = "inline";
        
            if((m_channelDiv.style.display != "none") && (m_changeChannelButton.value == "apply"))
            {
                m_channelInput.focus();
                m_channelInput.select();
            }
            else if(m_notesInputDiv.style.display != "none")
            {
                m_notesText.focus();
            }

            if (m_pViewer.isLive && m_arrNotes.length)
            {
                m_pEventViewer.UpdateStatus(m_arrNotes[m_arrNotes.length - 1]);
            }
        }
    }
    this.SetFocus = SetFocus;

    this.RenderContents = function()
    {
        // load user name from cookie
        m_pViewer.channel = getCookie("Channel");

        RenderControls();

        FillNotesUserSelect();

        for (var i in m_pViewer.publicNotesUsers)
        {
            if (m_pViewer.userName && (m_pViewer.publicNotesUsers[i] == m_pViewer.userName))
            {
                m_publicNotesToggleCheckbox.checked = true;
                m_publicNotesToggleLabel.className = "public";
            }
        }

        if (m_pViewer.userName || m_pViewer.channel)
        {
            m_channelInput.value = m_pViewer.channel;
            ToggleChangeChannelControl();
        }

        SetFocus();
    }

    this.UpdateStatus = function(status)
    {
        m_pLastEvent = status;

        if (!m_pEditingEvent)
        {
            m_pEventViewer.UpdateStatus(status);
        }
    }
}


function SearchViewer(el, pViewer)
{
    var m_el = el;
    var m_elSearchChrome = document.getElementById("searchChrome");
    
    var m_pViewer = pViewer;
    var m_arrResults = null;
    
    // we have an event viewer - we probably should subclass it instead
    var m_pEventViewer = new EventViewer(document.getElementById("searchResults" ), m_pViewer);
    
    // get our html elements
    var m_searchQuery = document.getElementById("searchQuery");    
    var m_context = document.getElementById("searchContext");
    var m_searchSlidesRadio = document.getElementById("searchSlidesRadio");
    var m_searchNotesRadio = document.getElementById("searchNotesRadio");
    var m_searchTranscriptRadio = document.getElementById("searchTranscriptRadio");
    var m_submitButton = document.getElementById("searchSubmitButton");
    var m_channelName = document.getElementById("channelName");

    function GetSearchType() {
        if (m_searchNotesRadio.checked) {
            return "notes";
        }
        else if (m_searchTranscriptRadio.checked) {
            return "transcript";
        }
        else {
            return "ppt";
        }
    }
    
    // add event handlers to detect when the user hits enter
    m_searchQuery.onkeypress = function(e)
    {
        e = GetEvent( e );
        
        // return if the user hasn't hit enter
        if( GetKey(e)!= 13 )
        {
            return true;
        }
        
        GetResults( m_searchQuery.value, GetSearchType() );
        return false;
    }
    
    m_submitButton.onclick = function(e)
    {               
        GetResults( m_searchQuery.value, GetSearchType() );
        return false;
    }
    
    m_searchNotesRadio.onmouseup = function(e)
    {               
        GetResults( m_searchQuery.value, "notes" );
        return false;
    }
    
    m_searchSlidesRadio.onmouseup = function(e)
    { 
        GetResults( m_searchQuery.value, "ppt" );
        return false;
    }

    m_searchTranscriptRadio.onmouseup = function(e) 
    {
        GetResults(m_searchQuery.value, "transcript");
        return false;
    }
    
    function GetResults( query, type )
    {
        function onResultsXML( pXML, bSuccess )
        {
            var error = "true";
            if( pXML )
            {
                error = SelectSingleNodeValue( pXML, "Error" );
            }

            if( error == "false" && bSuccess )
            {
                m_arrResults = new Array();
                var pEvents = SelectSingleNode( pXML, "Events" );
                var arrTimestamps = SelectNodes( pEvents, "SimpleTimestamp" );
            
                // parse the xml
                for (var i = 0; i < arrTimestamps.length; i++)
                {
                    m_arrResults.push( ParseEvent( arrTimestamps[i] ) );
                }
                
                // render
                m_pEventViewer.RenderContents( m_arrResults );
                
                // set our context
                if( m_arrResults.length > 0 )
                { 
                    if( type == "ppt" )
                    {
                        m_context.innerHTML = "Search returned <b>" + m_arrResults.length + "</b> slide results.";
                    }
                    else if( type == "notes" )
                    {
                        m_context.innerHTML = "Search returned <b>" + m_arrResults.length + "</b> note results.";
                    }
                    else 
                    {
                        m_context.innerHTML = "Search returned <b>" + m_arrResults.length + "</b> caption results.";
                    }
                }
                else
                {
                    m_context.innerHTML = "Search returned <b>no</b> results.";
                }
            }
            else
            {
                var errorSpan = CreateElement("SPAN", "error");
                SetText(errorSpan, "Error generating search results.");
                SetChild(m_context, errorSpan);
            }
        }

        // check to make sure we have a user, public notes selection or channel if we are searching notes
        if((type == "notes") && !(m_pViewer.userName || m_pViewer.notesUser || m_pViewer.channel))
        {
            alert("Please log in or select a channel in the notes tab before searching notes.");
            return;
        }
        
        if(query)
        {
            var params =
            {
                query:              encodeURIComponent(query),
                type:               type,
                id:                 m_pViewer.deliveryID,
                notesUser:          m_pViewer.notesUser,
                channelName:        (type == "notes" ? encodeURIComponent(m_pViewer.channel) : null),
                deliveryRelative:   (m_pViewer.isLive ? null : "true")
            };
                         
            CreateRequest(g_urlSearchResults, params, onResultsXML);
        }
        
        SetFocus();
    }
    
    this.SetVisible = function(bVisible)
    {
        SetVisible(m_el, bVisible);
    }

    function SetFocus()
    {
        m_searchQuery.select();
        m_searchQuery.focus();
    }
    this.SetFocus = SetFocus;
    
    this.OnResize = function(controlHeight)
    {
        var height = Math.max(controlHeight - g_cSearchChromeHeight - g_cPaneContentDividerWidth, g_cEventViewerMinHeight);
        m_pEventViewer.OnResize(height);
    }
    
    this.RenderContents = function(arrTimestamps)
    {
    }
    
    this.UpdateStatus = function(status)
    {
    }
}

function Questions(el, pViewer)
{
    var m_el = el;
    var m_pViewer = pViewer;

    var m_arrQuestions = new Array();

    // create a tab control to hold the single tab
    var m_pTabControl = new TabControl(document.getElementById("questionsTabBar"), null, false);
    m_pTabControl.AddView({ ViewID: "questions", Title: "Questions" });
    m_pTabControl.UpdateCurrentView();                            

    var m_pEventViewer = new EventViewer(document.getElementById("questionsAsked"), m_pViewer, { ShowUsernames: true });

    var m_pQuestionTextEntry = document.getElementById("questionTextEntry");
    m_pQuestionTextEntry.onkeypress = KeyPressed;
    m_pQuestionTextEntry.onfocus = HideQuestionTextEntryInstructions;

    var m_pQuestionTextEntryInstructions = document.getElementById("questionTextEntryInstructions");
    m_pQuestionTextEntryInstructions.onclick = HideQuestionTextEntryInstructions;

    var m_dQuestionStartTime = 0;

    function HideQuestionTextEntryInstructions()
    {
        m_pQuestionTextEntryInstructions.style.display = "none";
        m_pQuestionTextEntry.focus();
    }

    function KeyPressed(e)
    {
        e = GetEvent(e);

        // If user has hit [Enter], submit question.
        if (GetKey(e) == 13)
        {
            SubmitQuestion();

            // Prevent [Enter] from submitting form.
            return false;
        }

        // If user has hit [Esc], cancel edit or clear note entry
        if (GetKey(e) == 27)
        {
            m_pQuestionTextEntry.value = "";

            return false;
        }

        // Set the note start when the first character is typed in the notes area
        if (m_pQuestionTextEntry.value.length == 0)
        {
            // Since we're in a remote broadcast case, offset the note by an estimated delay to account for
            // latency between video capture and display.
            m_dQuestionStartTime = new Date().getTime() - g_dBroadcastLatencyEstimate;
        }

        // Allow character to be entered
        return true;
    }

    function SubmitQuestion()
    {
        if (!m_pViewer.userName)
        {
            alert("Please log in to ask questions.");
            return;
        }

        // Return if we have an empty note.
        var question = m_pQuestionTextEntry.value;
        if (question == "")
        {
            return;
        }
        m_pQuestionTextEntry.value = "";

        // Get the time difference between question start and current time
        // to determine start offset from server time.
        var questionStartTimeOffset = (m_dQuestionStartTime - new Date().getTime()) / 1000;

        var pQuestion = { Time:     questionStartTimeOffset,
                          UserName: m_pViewer.userName,
                          Caption:  question };

        function onQuestionSubmitted(pXML, bSuccess)
        {
            var ret = SelectSingleNodeValue(pXML, "ReturnCode");
            pQuestion.EventID = SelectSingleNodeValue(pXML, "EventID");
            // Retrieve event time from server.
            pQuestion.Time = SelectSingleNodeValue(pXML, "Time", "float");

            if ((ret == "true") && bSuccess)
            {
                // Add the event to the questions list and re-render our event viewer.
                m_arrQuestions.push(pQuestion);
                m_pEventViewer.RenderContents(m_arrQuestions);
                // Use pQuestion.Time to highlight added event.
                m_pEventViewer.UpdateStatus(pQuestion);
            }
        }

        var params =
        {
            deliveryid:         m_pViewer.deliveryID,
            time:               pQuestion.Time,
            data:               encodeURIComponent(pQuestion.Caption),
            channelName:        "Questions_" + m_pViewer.sessionPID
        };

        CreateRequest(g_urlNoteSubmit, params, onQuestionSubmitted);
    }

    this.SetVisible = function(bVisible)
    {
        SetVisible(m_el, bVisible);
    }

    this.OnResize = function(controlWidth)
    {
        m_el.style.height = g_dQuestionsHeight + "px";
        m_el.style.width = controlWidth + "px";

        m_pEventViewer.OnResize(g_dQuestionsHeight - g_dTabBarOffsetHeight - g_cPaneDefaultTopPadding - (g_cPaneContentDividerWidth * 2) - g_cQuestionEntryHeight);
    }

}