/**
 * Builds an accordion-style widget.
 * @author Adam J. McIntyre
 */
if(window.YAHOO) {
    YAHOO.namespace('widget.Accordion');

    /***
     * An Accodion animation.
     * @param {HTMLElement | String} el The element to receive accordion treatment.
     * @param {String} ajaxUrl (Optional) Url to send a request to for populating panes.
     * @param {Object} templateFunction (Optional) Object containing f (function) to call when data is loaded via Ajax and o (optional data object) to pass to that function.
     * @param {Object} animationSettings (Optional) Object containing speed (number) and frames (whether or not to use frames) for animating open blades.
     * @param {Boolean} silent (Optional) Determines whether or not accordion runs in "silent" mode: clicks on blades will not be wired up.
     */
    YAHOO.widget.Accordion = function(el, ajaxUrl, templateFunction, animationSettings, silent) {
        this.el = YAHOO.util.Dom.get(el);

        this.ajaxUrl = ajaxUrl || null;
        this.conn = null;
        this.animationSettings = animationSettings || { speed : .850, frames : true};
        this.silent = silent || false;

        this.currentBladeIndex = 0;    // Used primarily in "showAll()". Should find a better way to do that...
        this.bladeEls = [];

        this.onOpenCompleteEvent = new YAHOO.util.CustomEvent('onOpenComplete', this);
        this.onCloseCompleteEvent = new YAHOO.util.CustomEvent('onCloseComplete', this);
        this.onBeforeDataLoadEvent = new YAHOO.util.CustomEvent('onBeforeDataLoad', this);
        this.onDataLoadedEvent = new YAHOO.util.CustomEvent('onDataLoaded', this);
        this.onAfterDataLoadEvent = new YAHOO.util.CustomEvent('onAfterDataLoad', this);
        this.onTemplateCompleteEvent = new YAHOO.util.CustomEvent('onTemplateComplete', this);
        this.onAfterShowAllEvent = new YAHOO.util.CustomEvent('onShowAll', this);
        this.onAfterHideAllEvent = new YAHOO.util.CustomEvent('onHideAll', this);

        if(templateFunction) {
            this.onDataLoaded(function(e, args, o) {
                templateFunction.f(e, args, o);
                this.onTemplateCompleteEvent.fire(args, this);
            }, templateFunction.o);
        }

        this._bindClick();
    };

    var proto = YAHOO.widget.Accordion.prototype;

    proto.CLASSNAME = 'accordion';

    /***
     * Binds click events to Accordion el's children
     */
    proto._bindClick = function() {
        var that = this;
        this.bladeEls = YAHOO.util.Selector.query('#' + this.el.id + ' li.yui-accordion > div.hd');

        if(!this.silent) {
            YAHOO.util.Event.addListener(this.bladeEls, 'click', function() {
                var el = this;
                if(YAHOO.util.Dom.hasClass(el.parentNode, 'open')) {
                    that.closeBlade(el);
                }
                else {
                    that.openBlade(el);
                }
            });
        }
    }

    /***
     * Opens the blade associated with el
     * @param {HTMLElement} el Element we'd like to open.
     */
    proto.openBlade = function(el) {

        var that = this;
        var nextEl = YAHOO.util.Dom.getNextSibling(el);

        this.onBeforeDataLoadEvent.fire(this, el, nextEl);

        // possibly send request & populate data on callback
        // or open up body
        // (only send request after checking if we haven't already requested this data)
        if(YAHOO.util.Dom.hasClass(el.parentNode, 'remote') && ! YAHOO.util.Dom.hasClass(el.parentNode, 'loaded')) {
            var sUrl = this.ajaxUrl || el.parentNode.getAttribute('data-url');
            var callback = {
                cache:false,
                success : function(o) {
                    var cb = function(e, args) {
                        var targetEl = YAHOO.util.Dom.getFirstChild(args[0][0]);
                        var bdEl = args[0][0];
                        that._openBladeAnim(bdEl, function() {
                            // Toggle CSS classes
                            YAHOO.util.Dom.replaceClass(el.parentNode, 'closed', 'open');
                            YAHOO.util.Dom.addClass(el.parentNode, 'loaded');
                            YAHOO.util.Dom.setStyle(bdEl, 'filter', '');
                            that.onAfterDataLoadEvent.fire(this, el, args[0][0]);
                        }, el.parentNode.getAttribute('data-speed'));
                        // Kind of an odd one here. We're using a closure to make sure this
                        // callback is removed as an observer; otherwise, we'll wind up with
                        // every blade opening calling the callback function for every other
                        // blade that we've opened (they'll stick around as observers forever).
                        that.removeOnTemplateComplete(cb);
                    }

                    var f = function(e, args) {
                        cb();
                    }

                    var arg = { el : nextEl };

                    that.onTemplateComplete(cb);
                    that.onDataLoadedEvent.fire(nextEl, o);
                },
                failure : function(o) {
                    alert("We encountered an error requesting this data. Please try again.");
                },
                error : function(o) {
                    alert("We encountered an error requesting this data. Please try again.");
                }
            };
            //YAHOO.util.Connect.abort(that.conn);    // Abort any open requests
            this.conn = YAHOO.util.Connect.asyncRequest('GET', sUrl + '?' +
                                                               el.parentNode.getAttribute('data-params'), callback);
        }
        else {
            that._openBladeAnim(nextEl, function() {
                // Toggle CSS classes
                YAHOO.util.Dom.replaceClass(el.parentNode, 'closed', 'open');
                YAHOO.util.Dom.addClass(el.parentNode, 'loaded');
                if(YAHOO.env.ua.ie > 0) {
                    nextEl.style.removeAttribute('filter');
                }
                that.onAfterDataLoadEvent.fire(this, el, nextEl);
            }, el.parentNode.getAttribute('data-speed'));
        }
    }

    /***
     * Animates the opening of el.
     * @param {HTMLElement} el Element to open.
     * @param {Function} callback Callback to run once animation runs
     * @param {Number} sp (Optional) Speed attribute used to govern opening speed.
     */
    proto._openBladeAnim = function(el, callback, sp) {
        if(sp) {
            var speed = sp;
        }
        else {
            var speed = this.animationSettings.speed == 'adaptive' ?
                    (YAHOO.util.Dom.getFirstChild(el).offsetHeight / 1000) * .75 :
                    this.animationSettings.speed;
        }

        var anim = new YAHOO.util.Anim(el, {
            height : { to : YAHOO.util.Dom.getFirstChild(el).offsetHeight },
            opacity : { from : 0, to : 1.0 }
        },
                speed,
            //this.animationSettings.speed,
                YAHOO.util.Easing.easeIn);
        anim.setAttribute('useSeconds', this.animationSettings.frames);

        anim.onComplete.subscribe(function() {
            if(YAHOO.env.ua.ie > 0) {
                el.style.removeAttribute('filter');
            }
            callback();
        });

        anim.animate();

        this.onOpenCompleteEvent.fire(el);
    }

    /***
     * Closes the blade associated with el
     * @param {HTMLElement} el Element we'd like to close.
     */
    proto.closeBlade = function(el) {
        var nextEl = YAHOO.util.Dom.getNextSibling(el);

        // Toggle CSS class
        YAHOO.util.Dom.replaceClass(el.parentNode, 'open', 'closed');

        // close up div.bd
        var anim = new YAHOO.util.Anim(nextEl, {
            height : { to : 0 },
            opacity : { to : 0 }
        },
                0.25,
                YAHOO.util.Easing.easeOut);

        anim.animate();
        this.onCloseCompleteEvent.fire(this, el);
    }

    /***
     * Opens all blades in the accordion.
     */
    proto.showAll = function() {
        if(this.currentBladeIndex < this.bladeEls.length) {
            var el = this.bladeEls[this.currentBladeIndex];

            var that = this;
            if(this.currentBladeIndex == 0) {
                this.onAfterDataLoad(this._showAllCallback);
            }
            this.openBlade(el);
        }
        else {
            this.currentBladeIndex = 0;
            this.removeOnAfterDataLoad(this._showAllCallback);
            this.onAfterShowAllEvent.fire(this);
            return false;
        }
    }

    /***
     *  Callback function used to help throttle "showAll" requests.
     */
    proto._showAllCallback = function() {
        this.currentBladeIndex++;
        this.showAll();
    }

    /***
     * Closes all blades in the accordion.
     */
    proto.hideAll = function() {
        var els = YAHOO.util.Selector.query('#' + this.el.id + ' li > div.hd');
        for(var i = 0; i < els.length; i++) {
            this.closeBlade(els[i]);
        }
        this.onAfterHideAllEvent.fire(this);
    }

    /***
     * Subscribe a listener to the "onOpenComplete" event that fires when the accordion opens.
     * @param {Function} callback Function that executes when event fires.
     * @param {Object} obj (Optional) Optional data object.
     */
    proto.onOpenComplete = function(callback, obj) {
        this.onOpenCompleteEvent.subscribe(callback, obj);
    }

    /***
     * Subscribe a listener to the "onCloseComplete" event that fires when the accordion closes.
     * @param {Function} callback Function that executes when event fires.
     * @param {Object} obj (Optional) Optional data object.
     */
    proto.onCloseComplete = function(callback, obj) {
        this.onCloseCompleteEvent.subscribe(callback, obj);
    }

    /***
     * Subscribe a listener to the "onBeforeDataLoad" event that fires before the accordion fetches data.
     * @param {Function} callback Function that executes when event fires.
     * @param {Object} obj (Optional) Optional data object.
     */
    proto.onBeforeDataLoad = function(callback, obj) {
        this.onBeforeDataLoadEvent.subscribe(callback, obj);
    }

    /***
     * Subscribe a listener to the "onDataLoaded" event that fires when the accordion loads data via Ajax
     * for a given blade.
     * @param {Function} callback Function that executes when event fires.
     * @param {Object} obj (Optional) Optional data object.
     */
    proto.onDataLoaded = function(callback, obj) {
        this.onDataLoadedEvent.subscribe(callback, obj);
    }

    /***
     * Removes a listener from the "onDataLoaded" event.
     *
     * @param {Function} (Optional) callback Function that executes when event fires.
     * @param {Object} obj (Optional) Optional data object.
     */
    proto.removeOnDataLoaded = function(callback, obj) {
        this.onDataLoadedEvent.unsubscribe(callback, obj);
    };

    /***
     * Subscribe a listener to the "onAfterDataLoad" event that fires before the accordion fetches data.
     * @param {Function} callback Function that executes when event fires.
     * @param {Object} obj (Optional) Optional data object.
     */
    proto.onAfterDataLoad = function(callback, obj) {
        this.onAfterDataLoadEvent.subscribe(callback, obj);
    }

    /***
     * Removes a listener from the "onAfterDataLoad" event.
     * @param {Function} callback Function that executes when event fires.
     * @param {Object} obj (Optional) Optional data object.
     */
    proto.removeOnAfterDataLoad = function(callback, obj) {
        this.onAfterDataLoadEvent.unsubscribe(callback, obj);
    }

    /***
     * Subscribe a listener to the "onTemplateComplete" event that fires after an Ajax call's returned
     * data is "templated"
     * @param {Function} callback Function that executes when event fires.
     * @param {Object} obj (Optional) Optional data object.
     */
    proto.onTemplateComplete = function(callback, obj) {
        this.onTemplateCompleteEvent.subscribe(callback, obj);
    }

    /***
     * Removes a listener from the "onTemplateComplete" event.
     * @param {Function} callback Function that executes when event fires.
     * @param {Object} obj (Optional) Optional data object.
     */
    proto.removeOnTemplateComplete = function(callback, obj) {
        this.onTemplateCompleteEvent.unsubscribe(callback, obj);
    }

    /***
     * Subscribe a listener to the "onAfterShowAllEvent" event that fires after a "show all panels" action.
     * @param {Function} callback Function that executes when event fires.
     * @param {Object} obj (Optional) Optional data object.
     */
    proto.onAfterShowAll = function(callback, obj) {
        this.onAfterShowAllEvent.subscribe(callback, obj);
    }

    /***
     * Subscribe a listener to the "onAfterHideAllEvent" event that fires after a "hide all panels" action.
     * @param {Function} callback Function that executes when event fires.
     * @param {Object} obj (Optional) Optional data object.
     */
    proto.onAfterHideAll = function(callback, obj) {
        this.onAfterHideAllEvent.subscribe(callback, obj);
    }
}

