var g_dBroadcastLatencyEstimate = 13.0;

var g_cPaneDefaultTopPadding = 10;
var g_cPaneContentDividerWidth = 1;
var g_cQuestionEntryHeight = 30;

var g_cEventViewerMinHeight = 20;
var g_cTimeGroupSize = 3;

var g_cEventVisualHoverDelay = 250;

// 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,
        "tocSearchBox", 
        "tocSearchButton");
    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,
        "transSearchBox", 
        "transSearchButton");
    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)
        {
            // don't show empty captions in the toc
            var validEvents = new Array();
            for (var i = 0; i < arrEvents.length; i++)
            {
                if (arrEvents[i].Caption && (arrEvents[i].Caption.length > 0))
                {
                    validEvents.push(arrEvents[i]);
                }
            }

            if (validEvents.length > 0)
            {
                m_pTabControl.AddView(m_pContents);
                m_pContents.RenderContents(validEvents);
            }
        }

        // Transcript
        if (arrTranscriptEvents.length > 0)
        {
            m_pTabControl.AddView(m_pTranscript);
            m_pTranscript.RenderContents(arrTranscriptEvents);
        }
        else
        {
            document.getElementById("searchTranscript").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)
        {
            // make the event viewer a little larger than it would normally be
            g_dMinEventViewerWidth = 415;
            g_dMinViewerWidth = 1004;
            m_pViewer.Resize();
        }

        m_pTabControl.UpdateCurrentView();
    }

    this.UpdateStatus = function UpdateStatus(status)
    {
        m_pLastStatus = status;

        if (m_pSelectedView)
        {
            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";
    }

    this.CurrentlySelectedTab = function()
    {
        return m_pSelectedView ? m_pSelectedView.Title : null;
    }

    this.SearchCurrentEventTab = function (strQuery)
    {
        // needs to be in sync with searchTypeBox values
        var strType = null;
        switch (m_pSelectedView.ViewID)
        {
            case "Contents": strType = "ppt"; break;
            case "Transcript": strType = "transcript"; break;
            case "Notes": strType = "notes"; break;
        }

        if (m_pSelectedView != m_pSearch)
        {
            m_pTabControl.SelectView(m_pSearch);
        }

        m_pSearch.Search(strType, strQuery);
    }
}

// Info tab (presenter name, bio, etc.)
function InfoTab(el, pViewer)
{
    var m_el = el;
    var m_pViewer = pViewer;
    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;

        if (displayName && (displayName.length > 0))
        {
            // 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");
            SetTextWithLinks(bioText, bio);
            SetTextWithNewlineTranslation(bioText, bioText.innerHTML);
        }
    }

    this.RenderContents = function() { }
    
    this.UpdateStatus = function(status) { }
    
    this.SetVisible = function(bVisible)
    {
        SetVisible(m_el, bVisible);

        // resize
        if (bVisible)
        {
            m_pViewer.Resize();
        }
    }
    
    this.SetFocus = function() { }

    this.OnResize = function OnResize(controlHeight)
    {
        m_el.style.height = controlHeight + "px";
        m_elInfoContents.style.height = controlHeight + "px";
    }
}

function EventVisualGroup(elParent, eventTime, pViewer)
{
    var m_inlined = true;

    var m_bHovering = false;
    var m_bChildHovering = false;
    var m_bChildSelected = false;

    var m_groupDiv = CreateChildElement(elParent, "div", "eventVisualGroup");
    m_groupDiv.onmouseover = function (e)
    {
        m_bHovering = true;
        SetStyle(m_bChildSelected, m_bChildHovering);

        return false;
    }
    m_groupDiv.onmouseout = function (e)
    {
        m_bHovering = false;
        SetStyle(m_bChildSelected, m_bChildHovering);

        return false;
    }
    m_groupDiv.onclick = Clicker(
        m_groupDiv,
        function eventClick(pItem) { pViewer.SetVideoPosition(eventTime); },
        m_groupDiv);

    var m_eventVisualContainerDiv = CreateChildElement(m_groupDiv, "div", "eventVisualContainer");

    var m_timeDiv = CreateChildElement(m_eventVisualContainerDiv, "div", "eventVisualGroupTime");
    m_timeDiv.innerHTML = pViewer.isLive ? FormatFileTime(eventTime) : FormatRelativeTime(eventTime);

    this.AddEvents = function (arrEvents, pOptions)
    {
        var arrEventVisuals = new Array();
        if (arrEvents && (arrEvents.length > 0))
        {
            // if inlined, set the div to inline, otherwise block
            m_inlined = (arrEvents.length == 1);

            for (var i = 0; i < arrEvents.length; i++)
            {
                var eventDiv = m_inlined ? m_eventVisualContainerDiv : CreateChildElement(m_eventVisualContainerDiv, "div");

                var pEventVisual = new EventVisual(this, eventDiv, arrEvents[i], pViewer, pOptions);
                var container = pEventVisual.GetContainer();
                if (!pViewer.isLive)
                {
                    container.onclick = Clicker(
                        container,
                        function eventClick(pItem) { pViewer.SetVideoPosition(pItem.Time); },
                        arrEvents[i]);
                }
                else
                {
                    container.style.cursor = "normal";
                }

                arrEventVisuals.push(pEventVisual);
            }
        }

        return arrEventVisuals;
    }

    this.SetStyle = function (bChildSelected, bChildHovering)
    {
        SetStyle(bChildSelected, bChildHovering);
    }

    function SetStyle(bChildSelected, bChildHovering)
    {
        m_bChildHovering = bChildHovering;
        m_bChildSelected = bChildSelected;

        if (!m_inlined)
        {
            // highlight the group if hovering over the group, but not a child
            m_groupDiv.className =
                m_bHovering && !bChildHovering
                    ? "eventVisualGroupHover"
                    : "eventVisualGroup";
        }
        else
        {
            // if inlined, highlight the group when hovering/selecting a child
            m_groupDiv.className =
                bChildSelected
                    ? "eventVisualGroupSelected"
                    : bChildHovering
                        ? "eventVisualGroupHover"
                        : "eventVisualGroup";
        }
    }
}

// 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(pEventVisualGroup, 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_timerHover = null;
    var m_bThumbnailHighlighted = false;

    var eventInfoDiv = CreateChildElement(elParent, "div", "eventInfoDiv");

    if (pOptions && pOptions.ShowUsernames)
    {
        var divUser = CreateChildElement(eventInfoDiv, "div", "eventVisualUser");
        divUser.innerHTML = pEvent.UserName ? "[" + pEvent.UserName + "]" : "[anonymous]";
    }

    if (!m_pViewer.isStandaloneNotes
        && (document.getElementById("searchTypeBox").value == "all")
        && (m_pViewer.GetEventTabViewer().CurrentlySelectedTab() == "Search"))
    {
        var eventTargetType = (pEvent.EventTargetType != "PowerPoint") ? pEvent.EventTargetType : "Slides";
        var eventTypeSpan = CreateChildElement(eventInfoDiv, "span", "eventVisualType");
        eventTypeSpan.innerHTML = "[" + pEvent.EventTargetType + "]&nbsp;";
    }

    var divCaption = CreateChildElement(eventInfoDiv, "div", "eventVisualElement");
    SetTextWithLinks(divCaption, pEvent.Caption);

    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.
        elParent.style.position = "relative";

        m_elActionPanel = CreateChildElement(elParent, "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);
    }

    elParent.onmouseover = function(e)
    {
        m_bHovering = true;
        SetStyle();
        return false;
    }

    elParent.onmouseout = function(e)
    {
        m_bHovering = false;
        SetStyle();
        return false;
    }

    function SetStyle()
    {
        if (m_elActionPanel)
        {
            m_elActionPanel.style.visibility = 
                m_bHovering ? "visible" : "hidden";
        }

        if (m_bEditing)
        {
            elParent.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";
            }
        }
        else
        {
            elParent.className =
                m_bSelected 
                    ? "eventVisualSelected"
                    : m_bHovering 
                        ? "eventVisualHover"
                        : "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";
            }
        }

        // update style on the parent group
        pEventVisualGroup.SetStyle(m_bSelected, m_bHovering);

        if (!m_pViewer.isStandaloneNotes)
        {
            // highlight the corresponding thumbnail
            // IE doesn't like rapid div switching (it lags the video), so add a delay
            if (m_bHovering)
            {
                m_timerHover = setTimeout(function ()
                {
                    m_bThumbnailHighlighted = true;
                    pViewer.HighlightThumbnail(pEvent.Time, m_bHovering);
                }, g_cEventVisualHoverDelay);
            }
            else
            {
                clearTimeout(m_timerHover);
                if (m_bThumbnailHighlighted)
                {
                    m_bThumnailHighlighted = false;
                    pViewer.HighlightThumbnail(pEvent.Time, m_bHovering);
                }
            }
        }
    }

    this.SetSelected = function(bSelected, bEditing)
    {
        m_bSelected = bSelected;
        m_bEditing = bEditing;
        SetStyle();
    }

    this.GetContainer = function()
    {
        return elParent;
    }
}

// general viewer for events
function EventViewer(el, pViewer, pOptions, strSearchBoxId, strSearchButtonId)
{
    var m_el = el;
    var m_pViewer = pViewer;

    var m_divContents = CreateChildElement(m_el, "div");
    m_divContents.onmouseover = function () { m_bHovering = true; return false; }
    m_divContents.onmouseout = function () { m_bHovering = false; return false; }

    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;
    var m_bHovering = false;

    if (strSearchBoxId && strSearchButtonId)
    {
        var m_searchBox = document.getElementById(strSearchBoxId);
        m_searchBox.onkeypress = function (e)
        {
            e = GetEvent(e);
            if (GetKey(e) != 13)
            {
                // return if the user hasn't hit enter
                return true;
            }

            m_pViewer.GetEventTabViewer().SearchCurrentEventTab(m_searchBox.value);
            return false;
        }

        var m_searchButton = document.getElementById(strSearchButtonId);
        m_searchButton.onclick = function ()
        {
            m_pViewer.GetEventTabViewer().SearchCurrentEventTab(m_searchBox.value);
            return false;
        }
    }

    this.RenderContents = function (arrTimestamps)
    {
        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
        // first bucket them into groups based on time
        var timeHash = new Array();
        for (var i = 0; i < arrTimestamps.length; i++)
        {
            // calculate the bucket
            var eventTime = arrTimestamps[i].Time;
            var timeBucket = Math.round(eventTime / g_cTimeGroupSize);
            if (!timeHash[timeBucket])
            {
                timeHash[timeBucket] = new Array();
                timeHash[timeBucket].time = eventTime;
            }

            // add the event to the bucket
            timeHash[timeBucket].push(arrTimestamps[i]);
        }

        // now create a group for each bucket and add the events to the group and our list of visuals
        for (var timeBucket in timeHash)
        {
            var eventVisualGroup = new EventVisualGroup(m_divContents, timeHash[timeBucket].time, m_pViewer);
            m_arrVisuals = m_arrVisuals.concat(eventVisualGroup.AddEvents(timeHash[timeBucket], pOptions));
        }
    }

    // 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;

        if (!m_bHovering)
        {
            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) AND the transcript
function TocViewer(el, pViewer, strSearchBoxId, strSearchButtonId)
{
    var m_el = el;
    var m_pViewer = pViewer;

    var m_pEventViewer = new EventViewer(
        CreateChildElement(el, "div", "tocContents"),
        m_pViewer,
        null,
        strSearchBoxId,
        strSearchButtonId);

    this.RenderContents = function(arrTimestamps)
    {
        m_pEventViewer.RenderContents(arrTimestamps);
    }

    this.UpdateStatus = function(status)
    {
        m_pEventViewer.UpdateStatus(status);
    }

    this.SetVisible = function(bVisible)
    {
        SetVisible(m_el, bVisible);

        // resize
        if (bVisible)
        {
            m_pViewer.Resize();
        }
    }

    this.SetFocus = function() { }

    this.OnResize = function OnResize(controlHeight)
    {
        var headerHeight = Math.max(
            document.getElementById("tocSearchChrome").offsetHeight,
            document.getElementById("transSearchChrome").offsetHeight);

        var paneHeight = controlHeight - headerHeight;
        m_pEventViewer.OnResize(paneHeight);
    }
}

// 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
        },
        // Search is only available in full viewer.
        (!m_pViewer.isStandaloneNotes ? "notesSearchBox" : null),
        (!m_pViewer.isStandaloneNotes ? "notesSearchButton" : null));

    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_notesUserDiv = document.getElementById("notesUserDiv");
    var m_notesUserSelect = document.getElementById("notesUserSelect");
    var m_myNotesOptionGroup = document.getElementById("myNotesOptionGroup");
    var m_publicNotesOptionGroup = document.getElementById("publicNotesOptionGroup");

    // elements for choosing notes visibility
    var m_publicNotesToggle = document.getElementById("publicNotesToggle");
    var m_publicNotesIcon = document.getElementById("publicNotesIcon");

    // elements for choosing and displaying the notes channel
    var m_channelDiv = document.getElementById("channelDiv");
    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_notesText.onfocus = function (e)
    {
        if (!m_pEditingEvent)
        {
            m_notesText.rows = 3;
            // BUGBUG: we need to resize event panel, but no easy way to do it, so take a viewer resize pass
            m_pViewer.Resize();
        }
    }

    m_notesText.onblur = function (e)
    {
        m_notesText.rows = 1;
        // BUGBUG: we need to resize event panel, but no easy way to do it, so take a viewer resize pass
        m_pViewer.Resize();
    }

    document.getElementById("notesDivDisplay").onclick = function (e)
    {
        m_notesText.rows = 1;
        m_pViewer.Resize();
    }

    m_publicNotesToggle.onclick = function (e)
    {
        function onPublicToggled(pXML, bSuccess)
        {
            if (bSuccess && pXML)
            {
                // Page returns resulting value
                var retVal = SelectSingleNodeValue(pXML, "Public");
                m_publicNotesIcon.src = (retVal == "true") ? "../../Images/unlocked.png" : "../../Images/locked.png";
                m_publicNotesIcon.title = m_publicNotesIcon.alt = (retVal == "true")
                    ? "Your notes are public. Click to make them private."
                    : "Your notes are private. Click to make them public.";
                m_publicNotesToggle.title = (retVal == "true") ? "unlocked" : "locked";
            }
        }

        var params =
        {
            id: m_pViewer.deliveryID,
            public: (m_publicNotesToggle.title == "locked") // if locked, make public when the user clicks, and vice-versa
        }

        CreateRequest(g_urlNoteTogglePublic, params, onPublicToggled);
    }

    m_notesUserSelect.onchange = function (e)
    {
        var optionGroupName = m_notesUserSelect[m_notesUserSelect.selectedIndex].parentNode.id;
        m_pViewer.notesUser = null;
        m_pViewer.channel = null;

        // clear the cookie
        deleteCookie("Channel");

        if (optionGroupName == "myNotesOptionGroup")
        {
            // my notes group
            if (m_notesUserSelect.value == "#login")
            {
                // Use replace so we don't create a useless history state when logging in.
                location.replace(pViewer.loginURL);
            }
            else
            {
                PopulateNotesFromServer();
            }
        }
        else if (optionGroupName == "publicNotesOptionGroup")
        {
            // users group
            m_pViewer.notesUser = m_notesUserSelect.value;
            PopulateNotesFromServer();
        }
        else
        {
            // channel group
            if (m_notesUserSelect.value != "#custom")
            {
                // set the channel if it is not set to #custom, 
                // otherwise leave it blank and RenderControls() will show the channel input box
                pViewer.channel = m_notesUserSelect.value;
                PopulateNotesFromServer();
            }
        }

        RenderControls();
    }

    m_channelInput.onkeypress = function(e)
    {
        e = GetEvent( e );
        
        // return if the user hasn't hit enter
        if( GetKey(e)!= 13 )
        {
            return true;
        }
        
        SetCustomChannel();
        return false;
    }
    
    m_changeChannelButton.onclick = function(e)
    {
        SetCustomChannel();
        return false;
    }

    function SetCustomChannel()
    {
        if (m_channelInput.value)
        {
            m_pViewer.channel = m_channelInput.value;
            setCookie("Channel", m_pViewer.channel, 365);

            if (!selectDropdownValue(m_notesUserSelect, m_channelInput.value))
            {
                // add the channel to the dropdown if it doesn't exist
                var publicUserOption = CreateChildElement(document.getElementById("customNotesOptionGroup"), "option");
                publicUserOption.innerHTML = m_channelInput.value;
                publicUserOption.value = m_channelInput.value;

                selectDropdownValue(m_notesUserSelect, m_channelInput.value);
            }
        }
        else if (m_pViewer.userName)
        {
            selectDropdownValue(m_notesUserSelect, m_pViewer.userName);
        }
        else if (m_publicNotesOptionGroup.childNodes.length > 0)
        {
            selectDropdownValue(m_notesUserSelect, m_publicNotesOptionGroup.childNodes[0].value);
        }
        else
        {
            alert("Please enter a channel or log in to take notes.");
        }

        RenderControls();
    }

    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]));
                }

                // sort by time
                m_arrNotes.sort(function (a, b) { return a.Time - b.Time });

                // 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:        m_pViewer.channel ? encodeURIComponent(m_pViewer.channel) : null,
            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:        m_pViewer.channel ? encodeURIComponent(m_pViewer.channel) : null,
            deliveryRelative:   (m_pViewer.isLive ? null : "true")
        };
        
        CreateRequest( g_urlNoteSubmit, params, onNoteSubmitted );
    }

    this.SetVisible = function (bVisible)
    {
        SetVisible(m_el, bVisible);

        // resize
        if (bVisible)
        {
            m_pViewer.Resize();
        }
    }

    this.OnResize = function (controlHeight)
    {
        var headerHeight = document.getElementById("notesHeaderDiv").offsetHeight;
        var footerHeight = document.getElementById("notesInputDiv").offsetHeight;
        var eventViewerHeight = controlHeight - headerHeight - footerHeight - g_cPaneContentDividerWidth;

        m_pEventViewer.OnResize(eventViewerHeight);
    }

    function RenderControls()
    {
        var showDropDown = (m_notesUserSelect.value != "#custom");
        SetVisible(m_notesUserDiv, showDropDown);
        SetVisible(m_channelDiv, !showDropDown);

        var allowNotes = !pViewer.notesUser;
        SetVisible(m_notesInputDiv, allowNotes);

        var showPublicNotesToggle = allowNotes && pViewer.userName && !pViewer.channel && pViewer.bAllowPublishNotes;
        m_publicNotesToggle.style.visibility = showPublicNotesToggle ? "visible" : "hidden";

        m_pViewer.Resize();
        SetFocus();
    }

    function FillNotesUserSelect()
    {
        // add the private user notes option (or login if not logged in)
        var loginOption = CreateChildElement(m_myNotesOptionGroup, "option");
        loginOption.innerHTML = (m_pViewer.userName) ? m_pViewer.userName : "Log in...";
        loginOption.value = (m_pViewer.userName) ? m_pViewer.userName : "#login";

        // try to select the user's private notes by default
        // BUGBUG: make a dead state and populate by default?
        selectDropdownValue(m_notesUserSelect, m_pViewer.userName);

        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")
        {
            if (m_channelDiv.style.display != "none")
            {
                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");
        if (m_pViewer.channel)
        {
            // add the channel to the dropdown
            m_channelInput.value = m_pViewer.channel;
            SetCustomChannel();
        }

        FillNotesUserSelect();
        RenderControls();
    }

    this.UpdateStatus = function(status)
    {
        m_pLastEvent = status;

        if (!m_pEditingEvent)
        {
            m_pEventViewer.UpdateStatus(status);
        }
    }
}


function SearchViewer(el, pViewer)
{
    var m_el = el;
    
    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,
        null,
        "searchBox",
        "searchButton");
    
    // get our html elements
    var m_searchQuery = document.getElementById("searchBox");
    var m_context = document.getElementById("searchContext");

    var m_searchSortBox = document.getElementById("searchSortBox");
    var m_searchTypeBox = document.getElementById("searchTypeBox");

    m_searchSortBox.onchange = function() { GetResults(); return false; }
    m_searchTypeBox.onchange = function() { GetResults(); return false; }

    var m_channelName = document.getElementById("channelName");

    this.Search = function (strType, strQuery)
    {
        m_searchQuery.value = strQuery;

        if (strType)
        {
            m_searchTypeBox.value = strType;
        }

        GetResults();
    }

    function GetResults()
    {
        var query = m_searchQuery.value;
        var type = m_searchTypeBox.value;
        var sort = m_searchSortBox.value;

        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] ) );
                }

                // results come back in relevance order, sort by time if requested
                if (sort == "time")
                {
                    m_arrResults.sort(function (a, b) { return a.Time - b.Time });
                }
                
                // render
                m_pEventViewer.RenderContents(m_arrResults);

                // set our context
                var friendlyTypeName =
                    (type == "ppt")
                        ? " slide "
                        : (type == "notes")
                            ? " note "
                            : (type == " caption ")
                                ? " caption "
                                : " ";

                var friendlyResultCount = (m_arrResults.length > 0) ? m_arrResults.length : "no";
                m_context.innerHTML = "Search returned <b>" + friendlyResultCount + "</b>" + friendlyTypeName + "results."
                m_context.style.display = "inline-block";
            }
            else
            {
                var errorSpan = CreateElement("SPAN", "error");
                SetText(errorSpan, "Error generating search results.");
                SetChild(m_context, errorSpan);
            }

            // reflow as some elements might have changed height
            m_pViewer.Resize();
        }

        // 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:              query ? encodeURIComponent(query) : null,
                type:               type,
                id:                 m_pViewer.deliveryID,
                notesUser:          m_pViewer.notesUser,
                channelName:        m_pViewer.channel ? 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);

        // resize
        if (bVisible)
        {
            m_pViewer.Resize();
        }
    }

    function SetFocus()
    {
        m_searchQuery.select();
        m_searchQuery.focus();
    }
    this.SetFocus = SetFocus;
    
    this.OnResize = function OnResize(controlHeight)
    {
        var headerHeight = document.getElementById("searchSearchChrome").offsetHeight;
        var eventViewerHeight = Math.max(g_cEventViewerMinHeight, controlHeight - headerHeight);
        m_pEventViewer.OnResize(eventViewerHeight);
    }
    
    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);
    
        // resize
        if (bVisible)
        {
            m_pViewer.Resize();
        }
    }

    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_cPaneContentDividerWidth * 2) - g_cQuestionEntryHeight);
    }

}