/**
 * Builds and configures a set of dropdown menus that depend on each other to populate and
 * filter results.
 * @author Adam J. McIntyre
 */

YAHOO.namespace('widget.MultipleSelectItem');

/***
 * Constructor.
 * @param {String|HTMLElement} el Initial select element to bind this instance to.
 * @param {String} trigger "Trigger" value that determines when this Element should become active.
 * @param {Function} callback (Optional) Optional callback function to run onSelectChange.
 */
YAHOO.widget.MultipleSelectItem = function(el, trigger, callback) {
    this.el = YAHOO.util.Dom.get(el);
    this.trigger = trigger;
    this.id = this.el.id;
    this.children = [];
    this.triggers = [];    // Array of trigger values + objects
    this.ancestors = [];   // This might not be necessary...
    this.displayed = false;  // Is this item currently being displayed or not?

    var that = this;

    this.onSelectChangeEvent = new YAHOO.util.CustomEvent('onSelectChange', that);
    this.onSelectAddEvent = new YAHOO.util.CustomEvent('onSelectAdd', that);
    this.onTriggerEvent = new YAHOO.util.CustomEvent('trigger', that);

    if(callback) {
        this.onSelectChange(function(e, args, o) {
            callback(e, args, o);
        }, this);
    }

    YAHOO.util.Event.addListener(this.el, 'change', function(e, args) {
        if(this.options[this.selectedIndex].value != '') {
            that.onSelectChangeEvent.fire(args, this);
            that.onTriggerEvent.fire(args, this);
        }
    }, this);
};

var proto = YAHOO.widget.MultipleSelectItem.prototype;

/***
 * Determines whether a node of given id, elId, is a "child" of this MultipleSelectItem.
 * Example: If changing Select A creates Select B, Select B is a child of A.
 * @param {String} elId ID of element we're checking.
 */
proto.isChild = function(elId) {
    for(var i = 0; i < this.children.length; i++) {
        if(elId == this.children[i]) {
            return true;
        }
    }
    return false;
};

/***
 * Determines whether a node of given id, elId, is an "ancestor" of this MultipleSelectItem.
 * @param {String} elId ID of element we're checking.
 * @see YAHOO.widget.MultipleSelectItem.isChild
 */
proto.isAncestor = function(elId) {
    for(var i = 0; i < this.ancestors.length; i++) {
        if(elId == this.ancestors[i]) {
            return true;
        }
    }
    return false;
};

/***
 * Adds a new MutipleSelectItem to this Item's children.
 * @param {HTMLElement|String} el Select Element we'd like to be triggered by this parent.
 * @param {Function} trigger "Trigger" value that causes this Item's select box to display.
 * @param {Function} cb (Optional) Optional callback function to run onSelectChange.
 */
proto._addChild = function(el, trigger, cb) {
    var msi = new YAHOO.widget.MultipleSelectItem(el, trigger, cb);

    this.triggers[trigger] = msi;
    this.children.push(msi);
    msi.displayed = true;
    this.onSelectAddEvent.fire(this.arguments, msi);
    return msi;
};

/***
 * Adds a new MutipleSelectItem to this Item's children; these MSI items will have Options grouped into OptGroups.
 * @param {Array} gObj Array of Objects mapping arrays of Options to their OptGroup parents.
 * @param {Array} arrBefore (Optional) Children not belonging to a group. These will be placed before grouped children.
 * @param {Array} arrAfter (Optional) Children not belonging to a group. These will be placed after grouped children.
 * @param {String} (Optional) selId ID of Select Element we'll create
 * @param {Function} trigger "Trigger" value that causes this Item's select box to display.
 * @param {Function} cb (Optional) Optional callback function to run onSelectChange.
 */
proto._addGroupedChildren = function(gObj, arrBefore, arrAfter, selId, trigger, cb) {
    var sel = document.createElement('select');
    if(selId) {
        sel.id = selId;
    }
    else {
        sel.id = this.id + '_child_' + this.children.length;
    }

    sel.className = 'trackSelectionChange';

    if(arrBefore.length > 0) {
        for(var i = 0; i < arrBefore.length; i++) {
            sel.appendChild(this._addOption(arrBefore[i]));
        }
    }

    for(var i = 0; i < gObj.length; i++) {
        var og = document.createElement('optgroup');
        og.label = gObj[i].name;

        var cOpts = gObj[i].childOptions;
        for(var j = 0; j < cOpts.length; j++) {
            og.appendChild(this._addOption(gObj[i].childOptions[j]));
        }
        sel.appendChild(og);
    }

    if(arrAfter.length > 0) {
        for(var i = 0; i < arrAfter.length; i++) {
            sel.appendChild(this._addOption(arrAfter[i]));
        }
    }

    return this._addToDOM(sel, trigger, cb);
};

/***
 * Adds a new MutipleSelectItem to this Item's children.
 * @param {Array} arr Array of name/value pairs used to construct option tags.
 * @param {String} (Optional) selId ID of Select Element we'll create
 * @param {Function} trigger "Trigger" value that causes this Item's select box to display.
 * @param {Function} cb (Optional) Optional callback function to run onSelectChange.
 */
proto._addChildItems = function(arr, selId, trigger, cb) {
    var sel = document.createElement('select');
    if(selId) {
        sel.id = selId;
    }
    else {
        sel.id = this.id + '_child_' + this.children.length;
    }
    sel.className = 'trackSelectionChange';

    for(var i = 0; i < arr.length; i++) {
        sel.appendChild(this._addOption(arr[i]));
    }

    return this._addToDOM(sel, trigger, cb);
};

/***
 * Creates a single Option tag and returns it back.
 * @param {Object} opt Object representing Option tag's value, text, etc.
 */
proto._addOption = function(opt) {
    var optEl = document.createElement('option');
    optEl.value = opt.value;
    optEl.innerHTML = opt.text;
    if(opt.id) {
        optEl.id = opt.id;
    }
    if(opt.trigger) {
        optEl.setAttribute('data-trigger', opt.trigger);
    }
    if(opt.optional) {
        if(opt.optional.key) {
            optEl.setAttribute(opt.optional.key, opt.optional.value);
        } else {
            for(var i = 0; i < opt.optional.length; i++) {
                var o = opt.optional[i];
                optEl.setAttribute(o.key, o.value);
            }
        }
    }
    return optEl;
};

proto._addMenu = function(options, selId, trigger, cb) {
    var sel = document.createElement('select');
    if(selId) {
        sel.id = selId;
    } else {
        sel.id = this.id + '_child_' + this.children.length;
    }
    sel.className = 'trackSelectionChange';

    var menu = this._addToDOM(sel, trigger, cb);

    for(var i=0, opt_len=options.length; i<opt_len; i++) {
        var option = options[i];

        if(option.group) {
            sel.appendChild(menu._addGroup(sel, option, cb));
        } else {
            sel.appendChild(menu._newAddOption(sel, option, cb));
        }
    }

    menu.hideChildren();

    return menu;
};

proto._newAddOption = function(parent, opt, cb) {
    var optEl = document.createElement('option');
    optEl.value = opt.value;
    optEl.innerHTML = opt.text;
    if(opt.id) {
        optEl.id = opt.id;
    }
    if(opt.trigger) {
        optEl.setAttribute('data-trigger', opt.trigger);
    }
    if(opt.optional) {
        if(opt.optional.key) {
            optEl.setAttribute(opt.optional.key, opt.optional.value);
        } else {
            for(var i = 0; i < opt.optional.length; i++) {
                var o = opt.optional[i];
                optEl.setAttribute(o.key, o.value);
            }
        }
    }

    if(opt.sub_items) {
        this._addMenu(opt.sub_items, null, opt.trigger, cb);
        //this.hideChildren();
    }

    return optEl;
};

proto._addGroup = function(parent, opt, cb) {
    var optEl = document.createElement('optgroup');
    optEl.label = opt.group;

    if(opt.id) {
        optEl.id = opt.id;
    }

    var children = opt.children;

    for(var i=0, g_len=children.length; i<g_len; i++) {
        optEl.appendChild(this._newAddOption(parent, children[i], cb));
    }

    return optEl;
};

/***
 * Adds the Select box we've created to the DOM and wires up appropriate actions, fires events, etc.
 * @param {HTMLElement} sel Select box we're adding.
 * @param {String} trigger "Trigger" value that causes this Item's select box to display.
 * @param {Function} (Optional) cb Callback function to run on new MultipleSelectItem's onSelectChange Event.
 */
proto._addToDOM = function(sel, trigger, cb) {
    if(YAHOO.env.ua.webkit > 0) {    // Safari needs us to explicitly update parent's width to hold select
        var pNode = YAHOO.util.Dom.get(this.el).parentNode;
        pNode.style.overflow = "hidden";
        sel.style.display = 'none';
        pNode.style.width = '';
    }

    YAHOO.util.Dom.get(this.el).parentNode.appendChild(sel);

    if(YAHOO.env.ua.webkit > 0) {
        window.focus();
        sel.style.display = '';
        sel.focus();
    }
    var msi = new YAHOO.widget.MultipleSelectItem(sel.id, trigger, cb);

    this.triggers[trigger] = msi;
    this.children.push(msi);
    msi.displayed = true;
    this.onSelectAddEvent.fire(this.arguments, msi);
    return msi;
};

/***
 * Attempts to get an item that matches a given trigger
 * @param {String} Trigger value.
 */
proto._getChildItem = function(trigger) {
    if(this.triggers[trigger]) {
        return this.triggers[trigger];
    }
    return null;
};

/***
 * "Shows" the Element associated with this item.
 */
proto.showItem = function() {
    // Could use an onBeforeShow/onAfterShow?
    this.el.style.display = '';
    this.el.focus();    // Fix the "Webkit-sticks-these-anywhere" bug
    this.displayed = true;
    return this;
};

/***
 * "Shows" all children of this item.
 */
proto.showChildren = function() {
    for(var i = 0; i < this.children.length; i++) {
        this.children[i].showItem();
    }
};

/***
 * "Hides" the Element associated with this item and its children.
 */
proto.hideItem = function() {
    this.el.style.display = 'none';
    this.displayed = false;

    // Reset this item's selected index to the first item. Otherwise, there's a potential that
    // it could wind up having an "unselectable" current item.
    this.el.selectedIndex = 0;

    this.hideChildren();
    return this;
};

/***
 * Hides the display of all this Element's children.
 */
proto.hideChildren = function() {
    for(var i = 0; i < this.children.length; i++) {
        this.children[i].hideItem();
    }
};

/***
 * Subscribes callback to object's onSelectChange event.
 * @param {Function} callback Function to run once Event fires.
 * @param {Object} obj (Optional) Optional object to pass along.
 */
proto.onSelectChange = function(callback, obj) {
    this.onSelectChangeEvent.subscribe(callback, obj);
};

/***
 * Removes callback from object's onSelectChange event.
 * @param {Function} callback Function to run once Event fires.
 * @param {Object} obj (Optional) Optional object to pass along.
 */
proto.removeOnSelectChange = function(callback, obj) {
    this.onSelectChangeEvent.unsubscribe(callback, obj);
};

/***
 * Subscribes callback to object's onSelectAdd event.
 * @param {Function} callback Function to run once Event fires.
 * @param {Object} obj (Optional) Optional object to pass along.
 */
proto.onSelectAdd = function(callback, obj) {
    this.onSelectAddEvent.subscribe(callback, obj);
};

/***
 * Removes callback from object's onSelectAdd event.
 * @param {Function} callback Function to run once Event fires.
 * @param {Object} obj (Optional) Optional object to pass along.
 */
proto.removeOnSelectAdd = function(callback, obj) {
    this.onSelectAddEvent.unsubscribe(callback, obj);
};

YAHOO.namespace('widget.MultipleDD');

/***
 * Constructor.
 * @param {String|HTMLElement} rootEl Initial select element to bind this instance to. Needs an ID.
 * @param {Function} (Optional) cb Callback to run on el's onSelectChange Event.
 * @param {String} ajaxUrl (Optional) Url to send a request to for populating additional DDs.
 */
YAHOO.widget.MultipleDDMgr = function(rootEl, cb, ajaxUrl) {
    this.el = YAHOO.util.Dom.get(rootEl);

    // Create an initial item based on rootEl
    var initialMSItem = new YAHOO.widget.MultipleSelectItem(this.el, null, cb);

    this.managedSelects = [];
    this.managedSelects[this.el.id] = initialMSItem;
    /*
     this.displayedSelects = [];
     this.displayedSelects.push(initialMSItem);
     */

    //this.ajaxUrl = ajaxUrl || '';

    this.onSelectDisplayEvent = new YAHOO.util.CustomEvent('onSelectDisplay', this);
    this.onBeforeSelectDisplayEvent = new YAHOO.util.CustomEvent('onBeforeSelectDisplay', this);
};

proto = YAHOO.widget.MultipleDDMgr.prototype;

/***
 * Adds a Select Element to parent's children.
 * @param {HTMLElement} el Select Element.
 * @param {String} parentId ID of parent MultipleSelectItem
 * @param {String} trigger "Trigger" value that causes this Item's select box to display.
 * @param {Function} (Optional) cb Callback function to run on new MultipleSelectItem's onSelectChange Event.
 * @return MultipleSelectItem created and added with this call
 */
proto.addChild = function(el, parentId, trigger, cb) {
    this.onBeforeSelectDisplayEvent.fire(this, arguments);
    var item = this.managedSelects[parentId]._addChild(el, trigger, cb);
    this.managedSelects[item.id] = item;
    this.onSelectDisplayEvent.fire(this.arguments, item);
    return item.id;
};

/***
 * Adds new MutipleSelectItems to a Select list that is added to parent's children.
 * @param {Array} arr Array of name/value pairs used to construct option tags.
 * @param {String} (Optional) selId ID of Select Element we'll create
 * @param {String} parentId ID of parent MultipleSelectItem
 * @param {String} trigger "Trigger" value that causes this Item's select box to display.
 * @param {Function} (Optional) cb Callback function to run on new MultipleSelectItem's onSelectChange Event.
 * @return MultipleSelectItem created and added with this call
 */
proto.addSelectItems = function(arr, selId, parentId, trigger, cb) {
    this.onBeforeSelectDisplayEvent.fire(this, arguments);
    var item = this.managedSelects[parentId]._addChildItems(arr, selId, trigger, cb);
    this.managedSelects[item.id] = item;
    this.onSelectDisplayEvent.fire(this.arguments, item);
    return item.id;
};

/***
 * Adds new MutipleSelectItems to a Select list that is added to parent's children. Expects Optgroups.
 * @param {Object} gObj Object mapping arrays of Options to their OptGroup parents.
 * @param {Array} arrBefore (Optional) Children not belonging to a group. These will be placed before grouped children.
 * @param {Array} arrAfter (Optional) Children not belonging to a group. These will be placed after grouped children.
 * @param {String} (Optional) selId ID of Select Element we'll create
 * @param {String} parentId ID of parent MultipleSelectItem
 * @param {String} trigger "Trigger" value that causes this Item's select box to display.
 * @param {Function} (Optional) cb Callback function to run on new MultipleSelectItem's onSelectChange Event.
 * @return MultipleSelectItem created and added with this call
 */
proto.addGroupSelectItems = function(gObj, arrBefore, arrAfter, selId, parentId, trigger, cb) {
    this.onBeforeSelectDisplayEvent.fire(this, arguments);
    var item = this.managedSelects[parentId]._addGroupedChildren(gObj, arrBefore, arrAfter, selId, trigger, cb);
    this.managedSelects[item.id] = item;
    this.onSelectDisplayEvent.fire(this.arguments, item);
    return item.id;
};

proto.buildMenus = function(options, selId, parentId, trigger, cb) {
    this.onBeforeSelectDisplayEvent.fire(this, arguments);

    var sel = this.managedSelects[parentId]._addMenu(options, null, trigger, cb);
    this.managedSelects[sel.id] = sel;

    this.onSelectDisplayEvent.fire(this.arguments, sel);

    return sel;
};

/***
 * Hides all of parentId's children.
 * @param {String} parentId ID of parent MultipleSelectItem
 */
proto.hideSelects = function(parentId) {
    this.managedSelects[parentId].hideChildren();
};

/***
 * Subscribes callback to object's onSelectDisplay event.
 * @param {Function} callback Function to run once Event fires.
 * @param {Object} obj (Optional) Optional object to pass along.
 */
proto.onSelectDisplay = function(callback, obj) {
    this.onSelectDisplayEvent.subscribe(callback, obj);
};

/***
 * Removes callback from object's onSelectDisplay event.
 * @param {Function} callback Function to run once Event fires.
 * @param {Object} obj (Optional) Optional object to pass along.
 */
proto.removeOnSelectDisplay = function(callback, obj) {
    this.onSelectDisplayEvent.unsubscribe(callback, obj);
};

/***
 * Subscribes callback to object's onBeforeSelectDisplay event.
 * @param {Function} callback Function to run once Event fires.
 * @param {Object} obj (Optional) Optional object to pass along.
 */
proto.onBeforeSelectDisplay = function(callback, obj) {
    this.onBeforeSelectDisplayEvent.subscribe(callback, obj);
};

/***
 * Removes callback from object's onSelectDisplay event.
 * @param {Function} callback Function to run once Event fires.
 * @param {Object} obj (Optional) Optional object to pass along.
 */
proto.removeOnBeforeSelectDisplay = function(callback, obj) {
    this.onBeforeSelectDisplayEvent.unsubscribe(callback, obj);
};

