Source: common/util/multi_map.js

define(["lib/jquery", "lib/underscore", "./id_generator"],
function($,            _,                IDGenerator) {

    "use strict";

    var JQUERY_DATA_KEY = "lens:multimap:order";

    // Map of id -> Element
    var elementIds = {};

    // Gets the unique ID for an element by either reading its existing ID
    // or generating a new one.
    var getID = function(el) {
        if(!($(el).data("lens:multimap:id"))) {
            var id = IDGenerator.uuid();
            $(el).data("lens:multimap:id", id);
            elementIds[id] = el;
        }
        return $(el).data("lens:multimap:id");
    };

    // If obj is an Element, returns a unique id for the element. Else, returns
    // obj.toString()
    var hash = function(obj) {
        if(obj.nodeType) {
            return getID(obj);
        }
        else {
            return obj.toString();
        }
    };

    /**
     * Creates a new MultiMap
     *
     * @class
     * Maps keys to an array of values. Supports using either Strings or DOM
     * Elements as keys.
     *
     * If the keys are DOM elements, they can be ordered by setting the
     * `lens:multimap:order` jQuery data. See also {@link MultiMap.updateOrder},
     * which automatically creates a partial order for an array of elements.
     *
     * @name MultiMap
     * @private
     */
    var MultiMap = function() {
        /** @alias MultiMap.prototype */
        var self = {};

        var map = {};

        // given a map, returns an array of [key, value] pairs sorted by the
        // jQuery lens:multimap:order data on the key. Returns an underscore
        // wrapped array.
        var sortedMap = function(reverse) {
            return _.chain(map)
                    .map(function(value, key) {
                        // map key -> value pairs to [key, value] tuples, resolving
                        // element ids to elements
                        if(elementIds[key]) {
                            key = elementIds[key];
                        }

                        return [key, value];
                    })
                    .sortBy(function(pair) {
                        if(pair[0].nodeType) {
                            var order = $(pair[0]).data(JQUERY_DATA_KEY) || 0;
                            if(reverse) {
                                return -order;
                            }
                            else {
                                return order;
                            }
                        }
                        else {
                            return undefined;
                        }
                    });
        };

        /**
         * Gets the array of values associated with a key. This DOES NOT
         * make a copy; modification to the returned array will modify this
         * MultiMap.
         */
        self.get = function(key) {
            return map[hash(key)] || [];
        };

        /**
         * Adds the given value to the array of values associated with a key
         */
        self.put = function(key, value) {
            key = hash(key);

            if(key in map) {
                map[key].push(value);
            }
            else {
                map[key] = [value];
            }
            return value;
        };

        /**
         * Returns true iff the given key is in this MultiMap
         */
        self.include = function(key) {
            key = hash(key);

            return (key in map);
        };

        /**
         * Removes all instances of the given value from the array of values
         * associated with the given key.
         */
        self.removeVal = function(key, value) {
            key = hash(key);

            var values = map[key];
            if(!values) {
                return;
            }

            var newValues = _.without(values, value);

            if(newValues.length === 0) {
                // we're removing the last value
                delete map[key];
            }
            else {
                map[key] = newValues;
            }
        };

        /**
         * Removes the given key from this MultiMap.
         */
        self.delete = function(key) {
            delete map[key];
        };

        /**
         * For each key in this MultiMap that has at least one value, invokes
         * fn(value, keys). If the jquery data for the key element includes
         * a lens:multimap:order property, the keys are sorted by that order.
         */
        self.each = function(fn) {
            sortedMap().each(function(pair) {
                fn(pair[1], pair[0]);
            });
        };

        /**
         * The same as each, but traverses in reverse order.
         */
        self.eachReverse = function(fn) {
            sortedMap(true).each(function(pair) {
                fn(pair[1], pair[0]);
            });
        };

        /**
         * The same as each, but stops execution if the callback function
         * returns false. Returns true if the callback returned true every
         * time.
         */
        self.every = function(fn) {
            return sortedMap().every(function(pair) {
                return fn(pair[1], pair[0]);
            }).value();
        };

        /**
         * The same as every, but traverses in reverse order.
         */
        self.everyReverse = function(fn) {
            return sortedMap(true).every(function(pair) {
                return fn(pair[1], pair[0]);
            }).value();
        };

        /**
         * Returns an array that contains the first value associated with
         * each key in this MultiMap.
         */
        self.firsts = function() {
            return _.pluck(map, 0);
        };

        /**
         * Returns a sorted array of [key, value] pairs.
         */
        self.pairs = function() {
            return sortedMap().value();
        };

        /**
         * Returns the number of keys in this map.
         */
        self.size = function() {
            var size = 0;
            for(var key in map) {
                if(map.hasOwnProperty(key)) {
                    size++;
                }
            }

            return size;
        };

        return self;
    };

    _.extend(MultiMap, /** @lends MultiMap */ {
        /**
         * Updates the multimap order of the elements in the given list such
         * that no element has an order greater than an element that comes
         * after it.
         */
        updateOrder: function(array) {
            if(array.length === 0) {
                return;
            }

            // update first element
            var order = $(array[0]).data(JQUERY_DATA_KEY);
            if(!_.isNumber(order)) {
                order = 0;
                $(array[0]).data(JQUERY_DATA_KEY, 0);
            }

            // if length=1, we're done
            if(array.length === 1) {
                return;
            }

            // update the rest of the array
            for(var i = 1; i < array.length; i++) {
                var parentOrder = $(array[i]).data(JQUERY_DATA_KEY);
                if(!_.isNumber(parentOrder)) {
                    // parent has no multimap order; use child + 1
                    order++;
                    $(array[i]).data(JQUERY_DATA_KEY, order);
                }
                else {
                    // parent has a multimap order; use greater of child + 1 or its
                    // existing order
                    order = Math.max(order+1, parentOrder);
                    $(array[i]).data("lens:multimap:order", order);
                }
            }
        }
    });

    return MultiMap;
});