//
//  Tab.js
//  Written by Stefan Habel
//  Version 1.1
//  Last modified: 06.04.2008
//


//
// Class for Tab elements.
//
// \param name        the element's name
// \param triggerIds  a list of trigger element IDs
// \param contentIds  a list of content element IDs triggered by the trigger elements
// \param attributes  a collection of attributes to apply to the content elements
//
window.Tab = function ( name, triggerIds, contentIds, attributes )
{

    var freeSpaceMode = ContentElement.FreeSpaceMode.ANIMATED;
    if (triggerIds.length > 1)
        freeSpaceMode = ContentElement.FreeSpaceMode.DIRECTLY;

    this.name = name;
    this.SetAttributes(attributes);

    // create trigger elements
    this.triggers = this.CreateTriggerElements(triggerIds);

    // create content elements
//    console.group("Creating content elements for Tab \"" + name + "\"...");
    this.contentIds = this.CreateContentElements(triggerIds, contentIds, attributes.contentStates, attributes.style, freeSpaceMode, attributes.animateOpacity);
    console.groupEnd();

    // re-arrange content elements if necessary
    if (attributes.style == Tab.Style.HORIZONTAL)
        this.BuildHorizontalTable();

    // determine index of the selected elements
    var i = 0;
    this.selectedIndex = -1;
    while (i < attributes.contentStates.length && this.selectedIndex == -1)
        if (attributes.contentStates[i][0] == ContentElement.State.OPEN) {
            this.selectedIndex = i;
        } else
            ++i;
    console.info("Tab \"" + name + "\", index of selected element(s): " + this.selectedIndex);
    if (this.selectedIndex > -1)
        this.triggers[this.selectedIndex].SetSelected(true);
    // the index of the elements to select
    this.indexToSelect = 0;

    // animation properties
    //
    // list of content elements to animate
    this.contentsToAnimate = [];
    // the animation parameter
    this.t = 0.0;
    // flag that states whether content elements are to be faded out
    this.fadeOutNeeded = false;
    // flag that states whether space for content elements needs to be reserved or freed
    this.spaceAdjustmentNeeded = false;
    // flag that states whether content elements are to be faded in
    this.fadeInNeeded = false;
    // the phase in the current animation
    this.animationPhase = Animation.Phase.FADE_OUT;
    // flag that states whether the tab is currently animating
    this.isAnimating = false;
    // flag that states whether the tab's animation is reversed
    this.reverseAnimation = false;


    // store pointer to the object itself for use in functions that are called
    // by window.setTimeout or window.setInterval (in these functions 'this'
    // points to the browser's window object and not to the actual object)
    var object = this;


    //
    // Displays the current animation phase.
    //
    this.DisplayAnimationPhase = function ()
    {
        switch (object.animationPhase) {
            case Animation.Phase.FADE_OUT:
                console.info("Fading elements out...");
                break;
            case Animation.Phase.ADJUST_SPACE:
                console.info("Adjusting elements' spaces...");
                break;
            case Animation.Phase.FADE_IN:
                console.info("Fading elements in...");
                break;
            case Animation.Phase.CROSS_FADE:
                console.info("Cross-fading group elements...");
                break;
            case Animation.Phase.NONE:
                console.info("No animation.");
                break;
            default:
                console.info("Unknown animation phase.");
                break;
        }
    }


    //
    // Animates the content elements that are currently animated.
    //
    this.Animate = function ()
    {
        if (!object.reverseAnimation) {
            // normal animation
            object.t += Animation.DELTA;
            if (object.t > 1.0)
                object.t = 1.0;
        } else {
            // reversed animation
            object.t -= Animation.DELTA;
            if (object.t < 0.0)
                object.t = 0.0;
        }

        // ease-filter the animation parameter t
        var filteredT = Animation.EaseFilter(object.t);

        // iterate over all content elements to animate and animate them
        for (var i in object.contentsToAnimate)
            if (object.animationPhase == Animation.Phase.CROSS_FADE || object.contentsToAnimate[i].IsPhaseNeeded(object.animationPhase))
                object.contentsToAnimate[i].Animate(object.animationPhase, object.t, filteredT);

        if ((!object.reverseAnimation && object.t < 1.0)
        ||  (object.reverseAnimation  && object.t > 0.0))
            //> the current animation phase is still running
            // call this function again after a specific delay
            setTimeout(object.Animate, Animation.DELAY);
        else {
            //> the current animation phase has ended
            var oldAnimationPhase = object.animationPhase;
            object.animationPhase = Animation.Phase.NONE;
            switch (oldAnimationPhase) {
                case Animation.Phase.FADE_OUT:
                    if (object.spaceAdjustmentNeeded)
                        object.animationPhase = Animation.Phase.ADJUST_SPACE;
                    else if (object.fadeInNeeded)
                        object.animationPhase = Animation.Phase.FADE_IN;
                    break;
                case Animation.Phase.ADJUST_SPACE:
                    if (object.fadeInNeeded)
                        object.animationPhase = Animation.Phase.FADE_IN;
                    break;
                case Animation.Phase.FADE_IN:
                    break;
                case Animation.Phase.CROSS_FADE:
                    break;
            }
            if (object.animationPhase != Animation.Phase.NONE) {
                object.t = 0.0;
                object.DisplayAnimationPhase();
                // call this function again after a specific delay
                setTimeout(object.Animate, Animation.DELAY);
            } else {
                object.EndAnimation();
            }
        }
    }


    //
    // Ends the current animation of content elements.
    //
    this.EndAnimation = function ()
    {
        console.info("Ending animation...");
        // iterate over all animated content elements to tell them that the
        // animation has ended
        for (var i in this.contentsToAnimate)
            this.contentsToAnimate[i].EndAnimation(object.reverseAnimation);

        object.isAnimating = false;

        if (this.triggers.length > 1)
            object.selectedIndex = object.indexToSelect;

        window.currentSection = window.requestedSection.join(",").split(",");
    }


}


//
// Applies the given collection of attributes to the content elements represented
// by this Tab element.
//
// \param attributes  the collection of attributes to apply to the content elements
//
window.Tab.prototype.SetAttributes = function ( attributes )
{
    if (attributes.animationStyle != undefined)
        this.animationStyle = attributes.animationStyle;
    if (attributes.toggleAlreadySelected != undefined)
        this.toggleAlreadySelected = attributes.toggleAlreadySelected;

    if (this.triggers && this.triggers.length == 1) {
        //> single tab
        var contentIds = this.contentIds[0];
        for (var i in contentIds) {
            var contentElement = GetContentElement(contentIds[i]);
            if (contentElement)
                contentElement.SetAttributes(attributes);
        }
    }
}


//
// Creates a trigger element for each of the given IDs.
// Returns a list of trigger element objects for the given trigger IDs.
//
// \param triggerIds    the IDs to create trigger element objects for
//
window.Tab.prototype.CreateTriggerElements = function ( triggerIds )
{
    var result = [];

    var triggerId;
    var trigger;
    var element;

    for (var i in triggerIds) {

        triggerId = triggerIds[i];
        trigger = new TriggerElement(triggerId, TriggerElement.Action.TOGGLE);
        element = trigger.GetElement();

        // add event handler code
//        var enterCode = "tabs." + this.name + ".OnMouseEnter(\"" + triggerId + "\");";
//        var leaveCode = "tabs." + this.name + ".OnMouseLeave(\"" + triggerId + "\");";
        var clickCode = "tabs." + this.name + ".OnClick(\"" + triggerId + "\");";
//        eventHandlers.Add(triggerId, EventHandlers.Type.MOUSEOVER, enterCode, true);
//        eventHandlers.Add(triggerId, EventHandlers.Type.MOUSEOUT, leaveCode, true);
        eventHandlers.Add(triggerId, EventHandlers.Type.CLICK, clickCode, true);

        result.push(trigger);
    }

    return result;
}


//
// Creates content elements with the given IDs and using the given attributes.
//
// \param triggerIds        the IDs of trigger elements that trigger the content elements
// \param contentIds        the IDs of the content elements to create
// \param contentStates     a list of states for the content elements to create
// \param style             the style of the tab element
// \param freeSpaceMode     mode that determines how space for the element is reserved and freed
// \param animateOpacity    flag that states whether the element's opacity should be animated
// \return A list of content element IDs, whose order corresponds to the order of trigger elements in 'this.trigggers'.
//
window.Tab.prototype.CreateContentElements = function ( triggerIds, contentIds, contentStates, style, freeSpaceMode, animateOpacity )
{
    var result = {};

    var animationStyle;
    switch (style) {
        case Tab.Style.VERTICAL:
            animationStyle = Animation.Style.VERTICAL;
            break;
        case Tab.Style.HORIZONTAL:
            animationStyle = Animation.Style.HORIZONTAL;
            break;
        case Tab.Style.TAB:
            animationStyle = Animation.Style.NONE;
            break;
    }

    if (triggerIds.length == 1 && IsArray(contentIds) && !IsArray(contentIds[0]))
        contentIds = [contentIds];
    if (triggerIds.length == 1 && IsArray(contentStates) && !IsArray(contentStates[0]))
        contentStates = [contentStates];

    // traverse list of triggers
    for (var i in triggerIds) {
        if (!IsArray(contentIds[i]))
            contentIds[i] = [contentIds[i]];
        if (!IsArray(contentStates[i]))
            contentStates[i] = [contentStates[i]];

        result[i] = contentIds[i];

        for (var n in contentIds[i]) {
            var contentState = contentStates[i][n];
            if (contentState == undefined)
                contentState = contentStates[i][0];
            var attributes = {
                state: contentState,
                animationStyle: animationStyle,
                freeSpaceMode: freeSpaceMode,
                animateOpacity: animateOpacity
            };

            var contentId = contentIds[i][n];
            if (!window.contentElements[contentId]) {
                var contentElement = new ContentElement(contentId, attributes);
                window.contentElements[contentId] = contentElement;

//                console.info("Content element with ID \"" + contentId + "\" created. " + attributes.state);
            } else
                console.warn("Content element with ID \"" + contentId + "\" already exists.");
        }
    }
    
    return result;
}


//
// Returns the index of the trigger element with the given ID.
//
window.Tab.prototype.GetIndexByTriggerId = function ( triggerId )
{
    var result = -1;

    for (var i in this.triggers)
        if (this.triggers[i].GetElementId() == triggerId)
            return i;

    return result;
}


//
// Returns the ID of the currently selected trigger element.
//
window.Tab.prototype.GetSelectedTriggerId = function ()
{
    if (this.selectedIndex > -1 && this.selectedIndex < this.triggers.length)
        return this.triggers[this.selectedIndex].GetElementId();
    else
        return "";
}


//
// Rebuilds the DOM tree to add a table around the content elements with each
// content element contained in one of the table's TD cells in a single table
// row.
//
window.Tab.prototype.BuildHorizontalTable = function ()
{
    var parentNode;
    var dummyNode = document.createElement("br");
    var tableNode = document.createElement("table");

    if (false) {
        var borderAttr = document.createAttribute("border");
        borderAttr.nodeValue = "1";
        tableNode.setAttributeNode(borderAttr);
    }

    var cellpaddingAttr = document.createAttribute("cellpadding");
    cellpaddingAttr.nodeValue = "0";
    tableNode.setAttributeNode(cellpaddingAttr);

    var cellspacingAttr = document.createAttribute("cellspacing");
    cellspacingAttr.nodeValue = "0";
    tableNode.setAttributeNode(cellspacingAttr);

    var tbodyNode = document.createElement("tbody");

    var trNode = document.createElement("tr");

    var valignAttr = document.createAttribute("valign");
    valignAttr.nodeValue = "top";

    var tdNode = document.createElement("td");
    tdNode.setAttributeNode(valignAttr);

    // insert trigger elements into first column
    var element;
    for (i in this.triggers) {
        element = this.triggers[i].GetElement()
        tdNode.appendChild(element);
    }

    trNode.appendChild(tdNode);

    tbodyNode.appendChild(trNode);

    tableNode.appendChild(tbodyNode);

    var firstContentIds = this.contentIds[0];
    var firstContent = GetContentElement(firstContentIds[0]);
    if (!firstContent)
        return;
    var firstElement = firstContent.GetElement();
    parentNode = firstElement.parentNode;
    parentNode.insertBefore(dummyNode, element.nextSibling);
    var marginLeft = element.style.marginLeft;
    if (marginLeft != "") {
        var styleAttr = document.createAttribute("style");
        styleAttr.nodeValue = "margin-left: " + marginLeft;
        tableNode.setAttributeNode(styleAttr);
    }

    // iterate over trigger elements and insert the first of their associated
    // content elements into the second column
    var i = 0;
    while (i < this.triggers.length) {
        var contentIds = this.contentIds[i];
        var firstContent = GetContentElement(contentIds[0]);
        if (firstContent) {
            element = firstContent.GetElement();

            if (element.style.marginLeft != "")
                element.style.marginLeft = "";

            var valignAttr = document.createAttribute("valign");
            valignAttr.nodeValue = "top";

            var tdNode = document.createElement("td");
            tdNode.setAttributeNode(valignAttr);
            tdNode.appendChild(element);

            trNode.appendChild(tdNode);
        }
        ++i;
    }

    parentNode.replaceChild(tableNode, dummyNode);
}


//
// Event handler that is fired when the mouse pointer enters one of the Tab's
// trigger elements.
//
// \param triggerId     the ID of the trigger element that was entered
//
window.Tab.prototype.OnMouseEnter = function ( triggerId )
{
    console.info("Tab \"" + this.name + "\": Mouse has entered trigger \"" + triggerId + "\".");
}


//
// Event handler that is fired when the mouse pointer leaves one of the Tab's
// trigger elements.
//
// \param triggerId     the ID of the trigger element that was left
//
window.Tab.prototype.OnMouseLeave = function ( triggerId )
{
    console.info("Tab \"" + this.name + "\": Mouse has left trigger \"" + triggerId + "\".");
}


//
// Event handler that is fired when one of the Tab's trigger elements is
// clicked.
//
// \param triggerId     the ID of the trigger element that was clicked
//
window.Tab.prototype.OnClick = function ( triggerId )
{
    console.info("Tab \"" + this.name + "\": Trigger \"" + triggerId + "\" has been clicked.");

    // check which trigger element has been clicked
    var clickedTriggerIndex = this.GetIndexByTriggerId(triggerId);
    if (clickedTriggerIndex != -1) {
        var triggerElement = this.triggers[clickedTriggerIndex];
        var triggerTitle = triggerElement.GetTitle();

        // send click info to server
        // window.Ajax.Get("/tools/pageImpression.php?page=" + window.page + "&pageTitle=" + triggerTitle + "&user=" + window.user);
    }

    // leave the editing mode if it is currently activated
    if (window.editMode && window.leaveEditMode)
        window.leaveEditMode();

    if (this.triggers.length == 1) {
        //> single tab

        // check if elements are already animating
        if (this.isAnimating) {
            // toggle animation direction
            this.reverseAnimation = !this.reverseAnimation;
            return;
        }

        // determine content elements to animate or to toggle
        this.contentsToAnimate = [];
        // get list of content element IDs associated with clicked trigger element
        var contentIds = this.contentIds[0];
        // iterate over content element IDs
        for (var i in contentIds) {
            var id = contentIds[i];
            var contentElement = GetContentElement(id);
            if (contentElement)
                if (contentElement.IsAnimated()) {
                    this.contentsToAnimate.push(contentElement);
                } else {
                    contentElement.Toggle();
                }
        }
        if (this.contentsToAnimate.length > 0)
            this.BeginAnimation();
    } else {
        //> group tab

        // check if elements are already animating
        if (this.isAnimating)
            return;

        // get index of clicked element
        var index = this.GetIndexByTriggerId(triggerId);
        if (index == -1) {
            console.error("Tab \"" + this.name + "\": Index of trigger element \"" + triggerId + "\" could not be found.");
            return;
        }
        // check if the element is already selected
        if (index == this.selectedIndex) {
            console.info("Tab \"" + this.name + "\": The clicked element is already selected.");
            return;
        }
        // save index of clicked element as index of elements to select
        this.indexToSelect = index;

        // get trigger elements
        var selectedTrigger = this.triggers[this.selectedIndex];
        var triggerToSelect = this.triggers[this.indexToSelect];

        // change selection state of trigger elements's display
        selectedTrigger.SetSelected(false);
        triggerToSelect.SetSelected(true);


        // update location to reflect the section that is currently displayed
        var selectedTriggerId = selectedTrigger.GetElementId();
        var index = IndexOf(selectedTriggerId, window.currentSection);
        if (index == -1) {
            if (this.name == window.mainNavigationTab.name)
                window.requestedSection = [];
            // add the ID of the trigger element to select
            window.requestedSection.push(triggerId);
        } else {
            // replace the ID at the given index
            window.requestedSection[index] = triggerId;
            // remove the following IDs from the list
            window.requestedSection.splice(index + 1);
        }
        // check if there is a Tab element matching the first content ID
        var tabId = this.contentIds[this.indexToSelect][0];
        var tab = window.tabs[tabId];
        if (tab) {
            // determine which content element in the Tab is opened
            for (var i in tab.contentIds) {
                var firstContentId = tab.contentIds[i][0];
                var contentElement = GetContentElement(firstContentId);
                console.info(firstContentId);
                console.info(contentElement);
                if (contentElement && contentElement.IsOpen())
                    window.requestedSection.push(tab.triggers[i].GetElementId());
            }
        }
        if (window.requestedSection.join(",") != window.initialSection.join(","))
            location.hash = "#" + window.requestedSection.join(",") + ",";
//        else
//            location.hash = "#";

        // update the document's title using the caption of the trigger element to select
        var spans = triggerToSelect.GetElement().getElementsByTagName("SPAN");
        if (spans.length > 1)
            document.title = window.baseTitle + " - " + spans[1].firstChild.nodeValue;


        // build lists of content elements to animate
        this.contentsToAnimate = [];
        var contentElement;
        var selectedElementIds = this.contentIds[this.selectedIndex];
        for (var i in selectedElementIds) {
            contentElement = GetContentElement(selectedElementIds[i]);
            this.contentsToAnimate.push(contentElement);
        }
        var elementIdsToSelect = this.contentIds[this.indexToSelect];
        for (var i in elementIdsToSelect) {
            contentElement = GetContentElement(elementIdsToSelect[i]);
            if (contentElement.IsClosed())
                this.contentsToAnimate.push(contentElement);
        }

        console.info("Tab \"" + this.name + "\": Content elements to animate: " + this.contentsToAnimate + " (" + this.contentsToAnimate.length + " elements).");

        window.scrollTo(0, 0);

        this.BeginAnimation();
    }
}


//
// Updates element pointers to respect changes in the DOM tree.
//
window.Tab.prototype.UpdateElementPointers = function ()
{
    for (var i in this.contentIds) {
        // get list of content element IDs associated with clicked trigger element
        var contentIds = this.contentIds[i];
        // iterate over content element IDs
        for (var n in contentIds) {
            var id = contentIds[n];
            var contentElement = GetContentElement(id);
            if (contentElement) {
                contentElement.UpdateElementPointers();
                contentElement.UpdateElementLayout();
            }
        }
    }
}


//
// Initializes and starts the animation of the Tab's content elements to animate.
//
window.Tab.prototype.BeginAnimation = function ()
{
    if (this.contentsToAnimate.length == 0) {
        console.warn("Tab \"" + this.name + "\": There are no elements to animate.");
        return;
    }

    this.isAnimating = true;
    this.reverseAnimation = false;

    // iterate over all content elements to animate to tell them that they are
    // about to be animated
    for (var i in this.contentsToAnimate)
        this.contentsToAnimate[i].BeginAnimation();

    this.t = 0.0;
    this.animationPhase = Animation.Phase.NONE;

    if (this.triggers.length == 1) {
        //> single trigger

        this.fadeOutNeeded = false;
        this.spaceAdjustmentNeeded = false;
        this.fadeInNeeded = false;

        // check which phases are needed for animating the content elements
        var contentIds = this.contentIds[0];
        for (var i in contentIds) {
            var content = GetContentElement(contentIds[i]);
            if (content) {
                if (!this.fadeOutNeeded && content.IsPhaseNeeded(Animation.Phase.FADE_OUT))
                    this.fadeOutNeeded = true;
                if (!this.spaceAdjustmentNeeded && content.IsPhaseNeeded(Animation.Phase.ADJUST_SPACE))
                    this.spaceAdjustmentNeeded = true;
                if (!this.fadeInNeeded && content.IsPhaseNeeded(Animation.Phase.FADE_IN))
                    this.fadeInNeeded = true;
            }
        }

        // check which phase to perform first
        if (this.fadeOutNeeded)
            this.animationPhase = Animation.Phase.FADE_OUT;
        else if (this.spaceAdjustmentNeeded)
            this.animationPhase = Animation.Phase.ADJUST_SPACE;
        else if (this.fadeInNeeded)
            this.animationPhase = Animation.Phase.FADE_IN;
    } else
        // cross-fade group elements
        this.animationPhase = Animation.Phase.CROSS_FADE;

    this.DisplayAnimationPhase();

    if (this.animationPhase != Animation.Phase.NONE)
        this.Animate();
}



// style enumeration
window.Tab.Style = {
    VERTICAL   : 0,
    HORIZONTAL : 1
};
