// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

const Lang = imports.lang;
const System = imports.system;

const Params = imports.misc.params;

// This is an implementation of EcmaScript SameValue algorithm,
// which returns true if two values are not observably distinguishable.
// It was taken from http://wiki.ecmascript.org/doku.php?id=harmony:egal
//
// In the future, we may want to use the 'is' operator instead.
function _sameValue(x, y) {
    if (x === y) {
        // 0 === -0, but they are not identical
        return x !== 0 || 1 / x === 1 / y;
    }

    // NaN !== NaN, but they are identical.
    // NaNs are the only non-reflexive value, i.e., if x !== x,
    // then x is a NaN.
    // isNaN is broken: it converts its argument to number, so
    // isNaN("foo") => true
    return x !== x && y !== y;
}

const _hashers = {
    object: function(o) { return o ? System.addressOf(o) : 'null'; },
    function: function(f) { return System.addressOf(f); },
    string: function(s) { return s; },
    number: function(n) { return String(n); },
    undefined: function() { return 'undefined'; },
};

/* Map is meant to be similar in usage to ES6 Map, which is
   described at http://wiki.ecmascript.org/doku.php?id=harmony:simple_maps_and_sets,
   without requiring more than ES5 + Gjs extensions.

   Known differences from other implementations:
   Polyfills around the web usually implement HashMaps for
   primitive values and reversed WeakMaps for object keys,
   but we want real maps with real O(1) semantics in all cases,
   and the easiest way is to have different hashers for different
   types.

   Known differences from the ES6 specification:
   - Map is a Lang.Class, not a ES6 class, so inheritance,
     prototype, sealing, etc. work differently.
   - items(), keys() and values() don't return iterators,
     they return actual arrays, so they incur a full copy everytime
     they're called, and they don't see changes if you mutate
     the table while iterating
     (admittedly, the ES6 spec is a bit unclear on this, and
     the reference code would just blow up)
*/
const Map = new Lang.Class({
    Name: 'Map',

    _init: function(iterable) {
        this._pool = { };

        if (iterable) {
            for (let i = 0; i < iterable.length; i++) {
                let [key, value] = iterable[i];
                this.set(key, value);
            }
        }
    },

    _hashKey: function(key) {
        let type = typeof(key);
        return type + ':' + _hashers[type](key);
    },

    _internalGet: function(key) {
        let hash = this._hashKey(key);
        let node = this._pool[hash];

        if (node && _sameValue(node.key, key))
            return [true, node.value];
        else
            return [false, null];
    },

    get: function(key) {
        return this._internalGet(key)[1];
    },

    has: function(key) {
        return this._internalGet(key)[0];
    },

    set: function(key, value) {
        let hash = this._hashKey(key);
        let node = this._pool[hash];

        if (node) {
            node.key = key;
            node.value = value;
        } else {
            this._pool[hash] = { key: key, value: value };
        }
    },

    delete: function(key) {
        let hash = this._hashKey(key);
        let node = this._pool[hash];

        if (node && _sameValue(node.key, key)) {
            delete this._pool[hash];
            return [node.key, node.value];
        } else {
            return [null, null];
        }
    },

    keys: function() {
        let pool = this._pool;
        return Object.getOwnPropertyNames(pool).map(function(hash) {
            return pool[hash].key;
        });
    },

    values: function() {
        let pool = this._pool;
        return Object.getOwnPropertyNames(pool).map(function(hash) {
            return pool[hash].value;
        });
    },

    items: function() {
        let pool = this._pool;
        return Object.getOwnPropertyNames(pool).map(function(hash) {
            return [pool[hash].key, pool[hash].value];
        });
    },

    size: function() {
        return Object.getOwnPropertyNames(this._pool).length;
    },
});
