/* The contents of this file are subject to the Mozilla Public
 * License Version 1.1 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 * 
 * The Original Code is the Bugzilla Bug Tracking System.
 * 
 * The Initial Developer of the Original Code is Netscape Communications
 * Corporation. Portions created by Netscape are
 * Copyright (C) 1998 Netscape Communications Corporation. All
 * Rights Reserved.
 * 
 * Contributor(s): Christian Reis <kiko@async.com.br>
 */

/* this file contains functions to update form controls based on a
 * collection of javascript arrays containing strings */

/* selectClassification reads the selection from f.classification and updates
 * f.product accordingly
 *     - f: a form containing classification, product, component, varsion and
 *       target_milestone select boxes.
 * globals (3vil!):
 *     - prods, indexed by classification name
 *     - first_load: boolean, specifying if it is the first time we load
 *       the query page.
 *     - last_sel: saves our last selection list so we know what has
 *       changed, and optimize for additions.
 */
function selectClassification(classfield, product, component, version, milestone) {
    /* this is to avoid handling events that occur before the form
     * itself is ready, which could happen in buggy browsers.
     */
    if (!classfield) {
        return;
    }

    /* if this is the first load and nothing is selected, no need to
     * merge and sort all components; perl gives it to us sorted.
     */
    if ((first_load) && (classfield.selectedIndex == -1)) {
        first_load = false;
        return;
    }
    
    /* don't reset first_load as done in selectProduct.  That's because we
       want selectProduct to handle the first_load attribute
    */

    /* - sel keeps the array of classifications we are selected. 
     * - merging says if it is a full list or just a list of classifications 
     *   that were added to the current selection.
     */
    var merging = false;
    var sel = Array();

    /* if nothing selected, pick all */
    var findall = classfield.selectedIndex == -1;
    sel = get_selection(classfield, findall, false);
    if (!findall) {
        /* save sel for the next invocation of selectClassification() */
        var tmp = sel;
    
        /* this is an optimization: if we have just added classifications to an
         * existing selection, no need to clear the form controls and add 
         * everybody again; just merge the new ones with the existing 
         * options.
	 */
        if ((last_sel.length > 0) && (last_sel.length < sel.length)) {
            sel = fake_diff_array(sel, last_sel);
            merging = true;
        }
        last_sel = tmp;
    }
    /* save original options selected */
    var saved_prods = get_selection(product, false, true);

    /* do the actual fill/update, reselect originally selected options */
    updateSelect(prods, sel, product, merging);
    restoreSelection(product, saved_prods);
    selectProduct(product, component, version, milestone);
}


/* selectProduct reads the selection from the product control and
 * updates version, component and milestone controls accordingly.
 * 
 *     - product, component, version and milestone: form controls
 *
 * globals (3vil!):
 *     - cpts, vers, tms: array of arrays, indexed by product name. the
 *       subarrays contain a list of names to be fed to the respective
 *       selectboxes. For bugzilla, these are generated with perl code
 *       at page start.
 *     - first_load: boolean, specifying if it is the first time we load
 *       the query page.
 *     - last_sel: saves our last selection list so we know what has
 *       changed, and optimize for additions. 
 */
function selectProduct(product, component, version, milestone) {

    if (!product) {
        /* this is to avoid handling events that occur before the form
         * itself is ready, which could happen in buggy browsers. */
        return;
    }

    /* if this is the first load and nothing is selected, no need to
     * merge and sort all components; perl gives it to us sorted. */
    if ((first_load) && (product.selectedIndex == -1)) {
        first_load = false;
        return;
    }

    /* turn first_load off. this is tricky, since it seems to be
     * redundant with the above clause. It's not: if when we first load
     * the page there is _one_ element selected, it won't fall into that
     * clause, and first_load will remain 1. Then, if we unselect that
     * item, selectProduct will be called but the clause will be valid
     * (since selectedIndex == -1), and we will return - incorrectly -
     * without merge/sorting. */
    first_load = false;

    /* - sel keeps the array of products we are selected.
     * - merging says if it is a full list or just a list of products that
     *   were added to the current selection. */
    var merging = false;
    var sel = Array();

    /* if nothing selected, pick all */
    var findall = product.selectedIndex == -1;
    if (useclassification) {
        /* update index based on the complete product array */
        sel = get_selection(product, findall, true);
        for (var i=0; i<sel.length; i++) {
           sel[i] = prods[sel[i]];
        }
    } else {
        sel = get_selection(product, findall, false);
    }
    if (!findall) {
        /* save sel for the next invocation of selectProduct() */
        var tmp = sel;

        /* this is an optimization: if we have just added products to an
         *  existing selection, no need to clear the form controls and add
         *  everybody again; just merge the new ones with the existing
         *  options. */
        if ((last_sel.length > 0) && (last_sel.length < sel.length)) {
            sel = fake_diff_array(sel, last_sel);
            merging = true;
        }
        last_sel = tmp;
    }

    /* do the actual fill/update */
    if (component) {
        var saved_cpts = get_selection(component, false, true);
        updateSelect(cpts, sel, component, merging);
        restoreSelection(component, saved_cpts);
    }

    if (version) {
        var saved_vers = get_selection(version, false, true);
        updateSelect(vers, sel, version, merging);
        restoreSelection(version, saved_vers);
    }

    if (milestone) {
        var saved_tms = get_selection(milestone, false, true);
        updateSelect(tms, sel, milestone, merging);
        restoreSelection(milestone, saved_tms);
    }
}


/* updateSelect(array, sel, target, merging)
 *
 * Adds to the target select object all elements in array that
 * correspond to the elements selected in source.
 * - array should be a array of arrays, indexed by number. the
 *   array should contain the elements that correspond to that
 *   product.
 * - sel is a list of selected items, either whole or a diff
 *   depending on merging.
 * - target should be the target select object.
 * - merging (boolean) determines if we are mergine in a diff or
 *   substituting the whole selection. a diff is used to optimize adding
 *   selections.
 *
 * Example (compsel is a select form control)
 *
 *     var components = Array();
 *     components[1] = [ 'ComponentA', 'ComponentB' ];
 *     components[2] = [ 'ComponentC', 'ComponentD' ];
 *     source = [ 2 ];
 *     updateSelect(components, source, compsel, 0, 0);
 *
 * would clear compsel and add 'ComponentC' and 'ComponentD' to it.
 *
 */

function updateSelect(array, sel, target, merging) {

    var i, item;

    /* If we have no versions/components/milestones */
    if (array.length < 1) {
        target.options.length = 0;
        return false;
    }

    if (merging) {
        /* array merging/sorting in the case of multiple selections */
        /* merge in the current options with the first selection */
        item = merge_arrays(array[sel[0]], target.options, 1);

        /* merge the rest of the selection with the results */
        for (i = 1 ; i < sel.length ; i++) {
            item = merge_arrays(array[sel[i]], item, 0);
        }
    } else if ( sel.length > 1 ) {
        /* here we micro-optimize for two arrays to avoid merging with a
         * null array */
        item = merge_arrays(array[sel[0]],array[sel[1]], 0);

        /* merge the arrays. not very good for multiple selections. */
        for (i = 2; i < sel.length; i++) {
            item = merge_arrays(item, array[sel[i]], 0);
        }
    } else { /* single item in selection, just get me the list */
        item = array[sel[0]];
    }

    /* clear select */
    target.options.length = 0;

    /* load elements of list into select */
    for (i = 0; i < item.length; i++) {
        target.options[i] = new Option(item[i], item[i]);
    }
    return true;
}


/* Selects items in control that have index defined in sel
 *    - control: SELECT control to be restored
 *    - selnames: array of indexes in select form control */
function restoreSelection(control, selnames) {
    /* right. this sucks. but I see no way to avoid going through the
     * list and comparing to the contents of the control. */
    for (var j=0; j < selnames.length; j++) {
        for (var i=0; i < control.options.length; i++) {
            if (control.options[i].value == selnames[j]) {
                control.options[i].selected = true;
            }
        }
    }
}


/* Returns elements in a that are not in b.
 * NOT A REAL DIFF: does not check the reverse.
 *    - a,b: arrays of values to be compare. */
function fake_diff_array(a, b) {
    var newsel = new Array();
    var found = false;

    /* do a boring array diff to see who's new */
    for (var ia in a) {
        for (var ib in b) {
            if (a[ia] == b[ib]) {
                found = true;
            }
        }
        if (!found) {
            newsel[newsel.length] = a[ia];
        }
        found = false;
    }
    return newsel;
}

/* takes two arrays and sorts them by string, returning a new, sorted
 * array. the merge removes dupes, too.
 *    - a, b: arrays to be merge.
 *    - b_is_select: if true, then b is actually an optionitem and as
 *      such we need to use item.value on it. */
function merge_arrays(a, b, b_is_select) {
    var pos_a = 0;
    var pos_b = 0;
    var ret = new Array();
    var bitem, aitem;

    /* iterate through both arrays and add the larger item to the return
     * list. remove dupes, too. Use toLowerCase to provide
     * case-insensitivity. */
    while ((pos_a < a.length) && (pos_b < b.length)) {
        if (b_is_select) {
            bitem = b[pos_b].value;
        } else {
            bitem = b[pos_b];
        }
        aitem = a[pos_a];

        /* smaller item in list a */
        if (aitem.toLowerCase() < bitem.toLowerCase()) {
            ret[ret.length] = aitem;
            pos_a++;
        } else {
            /* smaller item in list b */
            if (aitem.toLowerCase() > bitem.toLowerCase()) {
                ret[ret.length] = bitem;
                pos_b++;
            } else {
                /* list contents are equal, inc both counters. */
                ret[ret.length] = aitem;
                pos_a++;
                pos_b++;
            }
        }
    }

    /* catch leftovers here. these sections are ugly code-copying. */
    if (pos_a < a.length) {
        for (; pos_a < a.length ; pos_a++) {
            ret[ret.length] = a[pos_a];
        }
    }

    if (pos_b < b.length) {
        for (; pos_b < b.length; pos_b++) {
            if (b_is_select) {
                bitem = b[pos_b].value;
            } else {
                bitem = b[pos_b];
            }
            ret[ret.length] = bitem;
        }
    }
    return ret;
}

/* Returns an array of indexes or values from a select form control.
 *    - control: select control from which to find selections
 *    - findall: boolean, store all options when true or just the selected
 *      indexes
 *    - want_values: boolean; we store values when true and indexes when
 *      false */
function get_selection(control, findall, want_values) {
    var ret = new Array();

    if ((!findall) && (control.selectedIndex == -1)) {
        return ret;
    }

    for (var i=0; i<control.length; i++) {
        if (findall || control.options[i].selected) {
            ret[ret.length] = want_values ? control.options[i].value : i;
        }
    }
    return ret;
}