/***********************************************************
 * Block-level element animated collapse/expand code
 * Adapted from/inspired by
 * http://www.harrymaugans.com/2007/03/06/how-to-create-an-animated-sliding-collapsible-div-with-javascript-and-css/
 * http://www.dhtmlgoodies.com/index.html?whichScript=show_hide_content_slide
 ***********************************************************/

var interval = 10; // update interval in ms
var maxDuration = 700; // maximum animation duration in ms
var minRate = 0.3; // minimum mean sliding rate in pixels/ms

// an array of all sliders
var sliders = new Array();

/**
 * Returns a random 16-digit numeric ID.
 */
function randomID() {
    var byte1 = Math.floor(Math.random() * 0x100000000);
    var byte2 = Math.floor(Math.random() * 0x100000000);
    return "" + byte1 + "" + byte2;
}

/**
 * Governs the rate of expansion (or "collapsion") as a function of
 * time. This function should take a single float argument between 0.0
 * and 1.0 representing the fraction of elapsed time, and should
 * return a float between 0.0 and 1.0 representing the fraction of
 * its full scalable height that the element should have. For proper
 * scaling the function must return 0.0 for an input of 0.0 and 1.0
 * for an input of 1.0.
 */
function expansionCurve(t) {
    // a*t + (b/2 - b/3 t)t^2 with a+b/6==1
    // a=0.5, b=3.0
    return 0.5 * t + (1.5 - 1.0 * t) * t * t;
}

/***********************************************************
 *                     class Slider
 ***********************************************************/

/**
 * A Javascript object that animates the sliding collapse or expansion
 * of an HTML block-level element.
 *
 * To use this class, create a new Slider object passing the HTML element
 * to slide as a parameter, then set properties on it if desired (e.g.
 * the callback function), and finally call slider.startSlide() to start
 * the animation. Once the animation is finished, the Slider object should
 * be discarded; it is not intended to be reused.
 *
 * There are a lot of animated Javascript sliders on the internet, but I
 * think this one is more versatile than nearly any other out there.
 * -Works with arbitrary block-level elements
 * -Directly computes slide progress as a function of time, so the animation
 *   doesn't run slow on a busy (or old) processor
 * -Slide position can be an arbitrary function of time (subject to appropriate
 *   boundary conditions) so the slide animation can include effects like
 *   varying speed or a bounce at the end, etc.
 * -Allows for a callback function once the slide is complete
 * -Allows any number of sliders to be running concurrently
 * -Supports sliding elements within other sliding elements
 */

// Variable prototypes, probably not strictly necessary
Slider.prototype.object;            // the HTML element to animate
Slider.prototype.sliderID;          // a unique ID for this slider
Slider.prototype.interval;          // the animation interval for this slider
Slider.prototype.currentHeight;     // tracks the current height of the element
Slider.prototype.initialHeight;     // the starting height of the element
Slider.prototype.targetHeight;      // the target/final height of the element
Slider.prototype.onfinish;          // a callback function for when the slide finishes
Slider.prototype.closure;           // a parameter for the callback
Slider.prototype.startTime;         // the time at which the slide started
Slider.prototype.timerID;           // a unique ID for the slider's timer
Slider.prototype.subElements;       // subelements to scroll

/**
 * Constructs a new Slider for the given element.
 *
 * @param object the DHTML node which should be expanded or collapsed
 */
function Slider(object) {
    this.object = object;
    if (this.object.attributes["dhtml_slider"]) {
        // a slider already exists for this object
        return;
    }
    // Assign this object as an attribute of the object we'll be sliding.
    // I don't think Javascript just allows object.dhtml_slider = this;
    // We could do that for divs if we put a div.prototype.dhtml_slider
    // statement at the top level of the code to create the dhtml_slider
    // propery for divs, but we want this to work for arbitrary block-level
    // elements as much as possible.
    this.object.attributes["dhtml_slider"] = this;

    // create a random ID
    this.sliderID = randomID();
    sliders[this.sliderID] = this;

    // Store the animation interval in the object (to allow for different
    // intervals for different objects in the future)
    this.interval = interval;
    this.duration = maxDuration;

    // Initialize the function to null
    this.onfinish = 0;
    this.closure = 0;
}

/**
 * Registers a callback to be called once the animation finishes.
 * The callback function accepts two arguments:
 * 1) false if the element was collapsed or true if it was expanded
 * 2) the closure argument passed to this function
 * Currently only one callback can be registered at a time.
 *
 * @param onfinish the callback function
 * @param closure a parameter to be passed to the callback
 */
Slider.prototype.setCallback = function(onfinish, closure) {
    // Store the callback
    this.onfinish = onfinish;
    this.closure = closure;
}

/**
 * Handles a single frame of the Slider animation. This function is
 * repeatedly invoked by setInterval.
 */
Slider.prototype.slideIncrement = function() {
    var expanding = this.targetHeight > this.initialHeight;
    var elapsed = new Date().getTime() - this.startTime;
    var ifactor = expansionCurve((elapsed > this.duration) ? 1.0 : 1.0 * elapsed / this.duration);
    this.currentHeight = Math.round(ifactor * this.targetHeight + (1.0 - ifactor) * this.initialHeight); // weighted average
    // check and see if the slide is over
    var slideOver = expanding ? (this.currentHeight >= this.targetHeight) : (this.currentHeight <= this.targetHeight);
    // now figure out how to distribute the current height among the elements
    if (slideOver) {
        this.stopSlide();
    }
    else {
        var remHeight = this.currentHeight - (expanding ? this.initialHeight : this.targetHeight);
        for (var i = 0; i < this.subElements.length; i++) {
            if (remHeight >= this.subElements[i].scrollHeight) {
                if (this.subElements[i].offsetHeight < this.subElements[i].scrollHeight) {
                    this.subElements[i].style.height = this.subElements[i].scrollHeight + "px";
                }
                remHeight -= this.subElements[i].scrollHeight;
            }
            else {
                // offsetHeight is unfortunately readonly - CSS seems to be the only way
                // to resize the element dynamically
                this.subElements[i].style.height = remHeight + "px";
                break; // would need to set remHeight = 0 if we didn't break here
            }
        }
    }
}

/** Starts the Slider animation. */
Slider.prototype.startSlide = function() {
    if (!this.sliderID) {
        return;
    }
    // the checks on scrollHeight and scrollWidth are a cheap hack, a substitute
    // for using the DOM window.getComputedStyle() function to check if the
    // effective display style is none
    // Note that in Firefox, the scrollHeight and scrollWidth of a hidden element
    // are 0, but in Konqueror they're undefined
    if (!(this.object.scrollHeight || this.object.scrollWidth) || (this.object.style && (this.object.style.display == "none"))) {
        // If the object is completely hidden, then we obviously need to expand it
        this.subElements = new Array();
        this.subElements[0] = this.object;
        this.object.style.height = "0px";
        this.object.style.display = this.object.tagName.toLowerCase() == "li" ? "list-item" : "block";
        // scrollHeight is a nonstandard but widely supported
        // property that tells the "natural height" of an element,
        // but it's 0 when the element is hidden
        this.targetHeight = this.object.scrollHeight;
        this.initialHeight = this.currentHeight = 0;
    }
    else {
        // If the object is not completely hidden, things get more complicated.
        // First we check and see if there are subnodes of our object which have
        // the "subcollapsible" class.
        var m = 0, n = 0;
        var expandedSubElements = new Array();
        var collapsedSubElements = new Array();
        var collapsibleHeight = 0;
        for (var i = 0; i < this.object.childNodes.length; i++) {
            subobj = this.object.childNodes[i];
            if (subobj.className && (subobj.className.indexOf("subcollapsible") >= 0)) {
                // as above, scroll* checks are a hack
                if (!(subobj.scrollHeight || subobj.scrollWidth) || (subobj.style && (subobj.style.display == "none"))) {
                    collapsedSubElements[m++] = subobj;
                    subobj.style.height = "0px";
                    subobj.style.display = subobj.tagName.toLowerCase() == "li" ? "list-item" : "block";
                }
                else {
                    expandedSubElements[n++] = subobj;
                }
                collapsibleHeight += subobj.scrollHeight;
            }
        }
        if (m > 0) {
            // there are collapsed subelements, so we're going to expand them
            this.subElements = collapsedSubElements;
            this.targetHeight = this.object.offsetHeight + collapsibleHeight;
            this.initialHeight = this.currentHeight = this.object.offsetHeight;
        }
        else if (n > 0) {
            // there are no collapsed subelements, so we're going to collapse them
            this.subElements = expandedSubElements;
            this.targetHeight = this.object.offsetHeight - collapsibleHeight;
            this.initialHeight = this.currentHeight = this.object.offsetHeight;
        }
        else {
            // there are no subelements with the subcollapsible class, so we're going
            // to collapse the parent object
            this.subElements = new Array();
            this.subElements[0] = this.object;
            this.targetHeight = 0;
            // offsetHeight is another nonstandard but widely supported property
            // which contains the actual height of the element, as a number,
            // including any borders (as does scrollHeight).
            this.initialHeight = this.currentHeight = this.object.offsetHeight;
        }
    }
//     alert("starting slide: init=" + this.initialHeight + ";target=" + this.targetHeight);
    var distance = Math.abs(this.targetHeight - this.initialHeight);
    if (1.0 * distance / this.duration < minRate) {
        this.duration = 1.0 * distance / minRate;
    }
    // Create the timer
    this.startTime = new Date().getTime();
    this.timerID = setInterval("sliders['" + this.sliderID + "'].slideIncrement()", interval);
}

/** Stops the Slider animation. */
Slider.prototype.stopSlide = function() {
    var expanding = (this.targetHeight > this.initialHeight);
    if (!expanding) {
        for (var i = 0; i < this.subElements.length; i++) {
            this.subElements[i].style.display = "none";
        }
    }
    for (var i = 0; i < this.subElements.length; i++) {
        this.subElements[i].style.removeProperty("height");
    }
    clearInterval(this.timerID);
    this.timerID = null;
    delete(this.object.attributes["dhtml_slider"]);
    delete(sliders[this.sliderID]);
    if (this.onfinish) {
        this.onfinish(expanding, this.closure);
    }
}

/***********************************************************
 * Helper functions for sliding things open/closed
 ***********************************************************/

// A struct for holding an HTML node and some text
Indicator.prototype.node;
Indicator.prototype.text;

/**
 * Creates a new indicator object (combination of HTML node and text)
 */
function Indicator(node, text) {
    this.node = node;
    this.text = text;
}

/**
 * Callback to toggle the status of the indicator.
 */
function toggleIndicator(open, indicator) {
    if (indicator.node.tagName == "img") {
        indicator.node.src = indicator.text;
    }
    else {
        indicator.node.innerHTML = indicator.text;
    }
}

/**
 * Toggles the display of a block-level element.
 *
 * @param slidingBlock the block-level element that slides open/closed
 * @param indicatorNode an element that changes to reflect the sliding
 *     block's state; may be null
 * @param indicatorOpenText the text content or image URL the indicator
 *     should use when the block-level element is open
 * @param indicatorClosedText the text content or image URL the indicator
 *     should use when the block-level element is closed
 */
function toggle(slidingBlock, indicatorNode, indicatorOpenText, indicatorClosedText) {
    var startClosed = false;
    if (slidingBlock.style && slidingBlock.style.display == "none") {
        startClosed = true;
    }
    else {
        var subobj;
        for (var i = 0; i < slidingBlock.childNodes.length; i++) {
            subobj = slidingBlock.childNodes[i];
            if ((subobj.className && subobj.className.indexOf("subcollapsible") >= 0) && (!(subobj.scrollHeight || subobj.scrollWidth) || (subobj.style && (subobj.style.display == "none")))) {
                startClosed = true;
                break;
            }
        }
    }
    var indicator = null;
    var slider = new Slider(slidingBlock);
    if (indicatorNode) {
        if (startClosed) {
            indicator = new Indicator(indicatorNode, indicatorOpenText);
            toggleIndicator(true, indicator);
        }
        else {
            indicator = new Indicator(indicatorNode, indicatorClosedText);
            slider.setCallback(toggleIndicator, indicator);
        }
    }
    slider.startSlide();
}

/**
 * Toggles the display of a block-level element, identified by ID.
 *
 * @param slidingBlockID the id of the block-level element that slides open/closed
 * @param indicatorNode an element that changes to reflect the sliding
 *     block's state; may be null
 * @param indicatorOpenText the text content or image URL the indicator
 *     should use when the block-level element is open
 * @param indicatorClosedText the text content or image URL the indicator
 *     should use when the block-level element is closed
 */
function toggleByID(slidingBlockID, indicatorNode, indicatorOpenText, indicatorClosedText) {
    var node = document.getElementById(slidingBlockID);
    if (node) {
        toggle(node, indicatorNode, indicatorOpenText, indicatorClosedText);
    }
}
