/**
 * Copyright 2012 Eric Wendelin - MIT License
 *
 * es6-map-shim.js is a DESTRUCTIVE shim that follows the latest Map specification
 * as closely as possible. It is destructive in the sense that it overrides native implementations.
 *
 * IMPORTANT: Currently, get(), set(), has() and delete() are all O(n) operations.
 * Normally, they would be O(1). Therefore it is NOT recommended to use this with
 * a large dataset or in any production environment.
 *
 * This library assumes ES5 functionality: Object.create, Object.defineProperty,
 * Array.indexOf, Function.bind and others.
 */
(function(module) {
    function Map(iterable) {
        var _items = [];
        var _keys = [];
        var _values = [];

        // Object.is polyfill, courtesy of @WebReflection
        var is = Object.is || function(a, b) {
            return a === b ?
                a !== 0 || 1 / a == 1 / b :
                a != a && b != b;
        };

        // More reliable indexOf, courtesy of @WebReflection
        var betterIndexOf = function(value) {
            if(value != value || value === 0) {
                for(var i = this.length; i-- && !is(this[i], value););
            } else {
                i = [].indexOf.call(this, value);
            }
            return i;
        };

        /**
         * MapIterator used for iterating over all entries in given map.
         *
         * @param map {Map} to iterate
         * @param kind {String} identifying what to yield. Possible values
         *      are 'keys', 'values' and 'keys+values'
         * @constructor
         */
        var MapIterator = function MapIterator(map, kind) {
            var _index = 0;

            return Object.create({}, {
                next: {
                    value: function() {
                        // check if index is within bounds
                        if (_index < map.items().length) {
                            switch(kind) {
                                case 'keys': return map.keys()[_index++];
                                case 'values': return map.values()[_index++];
                                case 'keys+values': return [].slice.call(map.items()[_index++]);
                                default: throw new TypeError('Invalid iterator type');
                            }
                        }
                        // TODO: make sure I'm interpreting the spec correctly here
                        throw new Error('Stop Iteration');
                    }
                },
                iterator: {
                    value: function() {
                        return this;
                    }
                },
                toString: {
                    value: function() {
                        return '[object Map Iterator]';
                    }
                }
            });
        };

        var _set = function(key, value) {
            // check if key exists and overwrite
            var index = betterIndexOf.call(_keys, key);
            if (index > -1) {
                _items[index][1] = value;
                _values[index] = value;
            } else {
                _items.push([key, value]);
                _keys.push(key);
                _values.push(value);
            }
        };

        var setItem = function(item) {
            if (item.length !== 2) {
                throw new TypeError('Invalid iterable passed to Map constructor');
            }

            _set(item[0], item[1]);
        };

        // FIXME: accommodate any class that defines an @@iterator method that returns
        //      an iterator object that produces two element array-like objects
        if (Array.isArray(iterable)) {
            iterable.forEach(setItem);
        } else if (iterable !== undefined) {
            throw new TypeError('Invalid Map');
        }

        return Object.create(MapPrototype, {
            /**
             * @return {Array} all entries in the Map, in order
             */
            items:{
                value:function() {
                    return [].slice.call(_items);
                }
            },
            /**
             * @return {Array} all keys in the Map, in order
             */
            keys:{
                value:function() {
                    return [].slice.call(_keys);
                }
            },
            /**
             * @return {Array} all values in the Map, in order
             */
            values:{
                value:function() {
                    return [].slice.call(_values);
                }
            },
            /**
             * Given a key, indicate whether that key exists in this Map.
             *
             * @param key {Object} expected key
             * @return {Boolean} true if key in Map
             */
            has:{
                value:function(key) {
                    // TODO: double-check how spec reads about null values
                    var index = betterIndexOf.call(_keys, key);
                    return index > -1;
                }
            },
            /**
             * Given a key, retrieve the value associated with that key (or undefined).
             *
             * @param key {Object}
             * @return {Object} value associated with key or undefined
             */
            get:{
                value:function(key) {
                    var index = betterIndexOf.call(_keys, key);
                    return index > -1 ? _values[index] : undefined;
                }
            },
            /**
             * Add or overwrite entry associating key with value. Always returns undefined.
             *
             * @param key {Object} anything
             * @param value {Object} also anything
             */
            set:{
                value: _set
            },
            /**
             * Return the number of entries in this Map.
             *
             * @return {Number} number of entries
             */
            size:{
                get:function() {
                    return _items.length;
                }
            },
            /**
             * Remove all entries in this Map. Returns undefined.
             */
            clear:{
                value:function() {
                    _keys.length = _values.length = _items.length = 0;
                }
            },
            /**
             * Delete entry with given key, if it exists.
             *
             * @param key {Object} any possible key
             * @return {Boolean} true if an entry was deleted
             */
            'delete':{
                value:function(key) {
                    var index = betterIndexOf.call(_keys, key);
                    if (index > -1) {
                        _keys.splice(index, 1);
                        _values.splice(index, 1);
                        _items.splice(index, 1);
                        return true;
                    }
                    return false;
                }
            },
            /**
             * Given a callback function and optional context, invoke the callback on all
             * entries in this Map.
             *
             * @param callbackFn {Function}
             */
            forEach:{
                value:function(callbackfn /*, thisArg*/) {
                    if (typeof callbackfn != 'function') {
                        throw new TypeError('Invalid callback function given to forEach');
                    }

                    function tryNext() {
                        try {
                            return iter.next();
                        } catch(e) {
                            return undefined;
                        }
                    }

                    var iter = this.iterator();
                    var current = tryNext();
                    var next = tryNext();
                    while(current !== undefined) {
                        callbackfn.apply(arguments[1], [current[1], current[0], this]);
                        current = next;
                        next = tryNext();
                    }
                }
            },
            /**
             * Return a MapIterator object for this map.
             */
            iterator:{
                value: function() {
                    return new MapIterator(this, 'keys+values');
                }
            },
            toString:{
                value: function() {
                    return '[Object Map]';
                }
            }
        });
    }

    var notInNode = module == 'undefined';
    var window = notInNode ? this : global;
    var module = notInNode ? {} : exports;
    var MapPrototype = Map.prototype;

    Map.prototype = MapPrototype = Map();

    window.Map = module.Map = window.Map || Map;
}.call(this, typeof exports));