/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This Source Code Form is "Incompatible With Secondary Licenses", as
 * defined by the Mozilla Public License, v. 2.0.
 */

// Functions to update form select elements based on a
// collection of javascript arrays containing strings.

/**
 * Reads the selected classifications and updates product, component,
 * version and milestone lists accordingly.
 *
 * @param  classfield Select element that contains classifications.
 * @param  product    Select element that contains products.
 * @param  component  Select element that contains components. Can be null if
 *                    there is no such element to update.
 * @param  version    Select element that contains versions. Can be null if
 *                    there is no such element to update.
 * @param  milestone  Select element that contains milestones. Can be null if
 *                    there is no such element to update.
 *
 * @global prods      Array of products indexed by classification name.
 * @global first_load Boolean; true if this is the first time this page loads
 *                    or false if not.
 * @global last_sel   Array that contains last list of products 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 lists; they are created 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.

    // Stores classifications that are selected.
    var sel = Array();

    // True if sel array has a full list or false if sel contains only
    // new classifications that are to be merged to the current list.
    var merging = false;

    // 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 elements and add
        // everything 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, null);

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

/**
 * Reads the selected products and updates component, version and milestone
 * lists accordingly.
 *
 * @param  product    Select element that contains products.
 * @param  component  Select element that contains components. Can be null if
 *                    there is no such element to update.
 * @param  version    Select element that contains versions. Can be null if
 *                    there is no such element to update.
 * @param  milestone  Select element that contains milestones. Can be null if
 *                    there is no such element to update.
 * @param  anyval     Value to use for a special "Any" list item. Can be null
 *                    to not use any. If used must and will be first item in
 *                    the select element.
 *
 * @global cpts       Array of arrays, indexed by product name. The subarrays
 *                    contain a list of components to be fed to the respective
 *                    select element.
 * @global vers       Array of arrays, indexed by product name. The subarrays
 *                    contain a list of versions to be fed to the respective
 *                    select element.
 * @global tms        Array of arrays, indexed by product name. The subarrays
 *                    contain a list of milestones to be fed to the respective
 *                    select element.
 * @global first_load Boolean; true if this is the first time this page loads
 *                    or false if not.
 * @global last_sel   Array that contains last list of products so we know what
 *                    has changed, and optimize for additions.
 */
function selectProduct(product, component, version, milestone, anyval) {
    // This is to avoid handling events that occur before the form
    // itself is ready, which could happen in buggy browsers.
    if (!product)
        return;

    // Do nothing if no products are defined. This is to avoid the
    // "a has no properties" error from merge_arrays function.
    if (product.length == (anyval != null ? 1 : 0))
        return;

    // If this is the first load and nothing is selected, no need to
    // merge and sort all lists; they are created 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;

    // Stores products that are selected.
    var sel = Array();

    // True if sel array has a full list or false if sel contains only
    // new products that are to be merged to the current list.
    var merging = false;

    // If nothing is selected, or the special "Any" option is selected
    // which represents all products, then pick all products so we show
    // all components.
    var findall = (product.selectedIndex == -1
                   || (anyval != null && product.options[0].selected));

    if (useclassification) {
        // Update index based on the complete product array.
        sel = get_selection(product, findall, true, anyval);
        for (var i=0; i<sel.length; i++)
           sel[i] = prods[sel[i]];
    }
    else {
        sel = get_selection(product, findall, false, anyval);
    }
    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, null);
        updateSelect(cpts, sel, component, merging, anyval);
        restoreSelection(component, saved_cpts);
    }

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

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

/**
 * Adds to the target select element all elements from array that
 * correspond to the selected items.
 *
 * @param array   An array of arrays, indexed by number. The array should
 *                contain elements for each selection.
 * @param sel     A list of selected items, either whole or a diff depending
 *                on merging parameter.
 * @param target  Select element that is to be updated.
 * @param merging Boolean that determines if we are merging in a diff or
 *                substituting the whole selection. A diff is used to optimize
 *                adding selections.
 * @param anyval  Name of special "Any" value to add. Can be null if not used.
 * @return        Boolean; true if target contains options or false if target
 *                is empty.
 *
 * Example (compsel is a select form element):
 *
 *     var components = Array();
 *     components[1] = [ 'ComponentA', 'ComponentB' ];
 *     components[2] = [ 'ComponentC', 'ComponentD' ];
 *     source = [ 2 ];
 *     updateSelect(components, source, compsel, false, null);
 *
 * This would clear compsel and add 'ComponentC' and 'ComponentD' to it.
 */
function updateSelect(array, sel, target, merging, anyval) {
    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 current selection.
    target.options.length = 0;

    // Add special "Any" value back to the list.
    if (anyval != null)
        target.options[0] = new Option(anyval, "");

    // Load elements of list into select element.
    for (i = 0; i < item.length; i++)
        target.options[target.options.length] = new Option(item[i], item[i]);

    return true;
}

/**
 * Selects items in select element that are defined to be selected.
 *
 * @param control  Select element of which selected options are to be restored.
 * @param selnames Array of option names to select.
 */
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.
 *
 * @param  a First array to compare.
 * @param  b Second array to compare.
 * @return   Array of elements in a but not in b.
 */
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.
 *
 * @param  a           First array to merge.
 * @param  b           Second array or an optionitem element to merge.
 * @param  b_is_select Boolean; true if b is an optionitem element (need to
 *                     access its value by item.value) or false if b is a
 *                     an array.
 * @return             Merged and sorted array.
 */
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)) {
        aitem = a[pos_a];
        if (b_is_select)
            bitem = b[pos_b].value;
        else
            bitem = b[pos_b];

        // 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, include 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 of options in a select form element.
 *
 * @param  control     Select form element from which to find selections.
 * @param  findall     Boolean; true to return all options or false to return
 *                     only selected options.
 * @param  want_values Boolean; true to return values and false to return
 *                     indexes.
 * @param  anyval      Name of a special "Any" value that should be skipped. Can
 *                     be null if not used.
 * @return             Array of all or selected indexes or values.
 */
function get_selection(control, findall, want_values, anyval) {
    var ret = new Array();

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

    for (var i = (anyval != null ? 1 : 0); i < control.length; i++)
        if (findall || control.options[i].selected)
            ret[ret.length] = want_values ? control.options[i].value : i;

    return ret;
}
