/*jslint unparam: true, vars: true, white: true, newcap: true, nomen: true, plusplus: true, maxerr: 50, indent: 4 */

/*global window,Hashtable*/

// Mutable hashtables.


(function (baselib, Hashtable) {
    'use strict';
    var exports = {};

    baselib.hashes = exports;


    
    var _eqHashCodeCounter = 0;
    var makeEqHashCode = function () {
        _eqHashCodeCounter++;
        return String(_eqHashCodeCounter);
    };


    // getEqHashCode: any -> string
    // Given a value, produces a hashcode appropriate for eq.
    var getEqHashCode = function (x) {
        if (typeof (x) === 'string') {
            return x;
        }
        if (typeof (x) === 'number') {
            return String(x);
        }
        if (x && !x._eqHashCode) {
            x._eqHashCode = makeEqHashCode();
        }
        if (x && x._eqHashCode) {
            return x._eqHashCode;
        }
        return '';
    };


    // getEqvHashCode: any -> string
    var getEqvHashCode = function (x) {
        if (baselib.numbers.isNumber(x)) {
            return baselib.numbers.toFixnum(x);
        }
        if (baselib.chars.isChar(x)) {
            return x.val;
        } 
        return getEqHashCode(x);
    };


    var eq = function (x, y) { return x === y; };
    var eqv = baselib.equality.eqv;
    var equal = function (x, y) {
        return baselib.equality.equals(x, y, new baselib.UnionFind()); 
    };


    // Creates a low-level hashtable, following the interface of 
    // http://www.timdown.co.uk/jshashtable/
    var makeLowLevelEqHash = function () {
        return new Hashtable(getEqHashCode,
                             function (x, y) { return x === y; });
    };


    var makeEqHashtable = function() { 
        return new WhalesongHashtable(
            "hasheq",
            getEqHashCode,
            eq,
            new Hashtable(getEqHashCode, eq));
    };

    var makeEqualHashtable = function() {
        return new WhalesongHashtable(
            "hash",
            getEqualHashCode,
            equal,
            new Hashtable(getEqualHashCode, equal));
    };
    
    
    var makeEqvHashtable = function() {
        return new WhalesongHashtable(
            "hasheqv",
            getEqvHashCode,
            eqv,
            new Hashtable(getEqvHashCode, eqv));
    };


    var makeImmutableEqHashtable = function() { 
        return makeEqHashtable().toImmutable();
    };

    var makeImmutableEqualHashtable = function() {
        return makeEqualHashtable().toImmutable();
    };
        
    var makeImmutableEqvHashtable = function() {
        return makeEqvHashtable().toImmutable();
    };


    // When we need to make comparators for the immutable hash tables, use this.
    var makeComparator = function(hash, eq) {
        return function(x, y) {
            var hx = hash(x), hy = hash(y);
            if (hx < hy) { return -1; }
            if (hx > hy) { return 1; }
            
            if (eq(x, y)) { return 0; }

            hx = getEqHashCode(x);
            hy = getEqHashCode(y);
            if (hx < hy) { return -1; }
            if (hx > hy) { return 1; }
            return 0;
        }
    };


    //////////////////////////////////////////////////////////////////////
    // Whalesong's Hashtables are a thin wrapper around the mutable Hashtable
    // class to make it printable and equatable.
    var WhalesongHashtable = function (type, hash_function, equality_function, hash) {
        this.type = type;
        this.hash_function = hash_function;
        this.equality_function = equality_function;
        this.hash = hash;
    };

    WhalesongHashtable.prototype.clone = function() {
        return new WhalesongHashtable(this.type, this.hash_function, this.equality_function, this.hash.clone());
    };

    WhalesongHashtable.prototype.size = function() {
        return this.hash.size();
    };

    WhalesongHashtable.prototype.toWrittenString = function (cache) {
        var keys = this.hash.keys();
        var ret = [], i;
        for (i = 0; i < keys.length; i++) {
            var keyStr = baselib.format.toWrittenString(keys[i], cache);
            var valStr = baselib.format.toWrittenString(this.hash.get(keys[i]), cache);
            ret.push('(' + keyStr + ' . ' + valStr + ')');
        }
        return ('#' + this.type + '(' + ret.join(' ') + ')');
    };
    
    WhalesongHashtable.prototype.toDisplayedString = function (cache) {
        var keys = this.hash.keys();
        var ret = [], i;
        for (i = 0; i < keys.length; i++) {
            var keyStr = baselib.format.toDisplayedString(keys[i], cache);
            var valStr = baselib.format.toDisplayedString(this.hash.get(keys[i]), cache);
            ret.push('(' + keyStr + ' . ' + valStr + ')');
        }
        return ('#' + this.type + '(' + ret.join(' ') + ')');
    };

    WhalesongHashtable.prototype.keys = function() {
        return this.hash.keys();
    };

    WhalesongHashtable.prototype.values = function() {
        return this.hash.values();
    };


    WhalesongHashtable.prototype.equals = function (other, aUnionFind) {
        if (!(other instanceof WhalesongHashtable)) {
            return false; 
        }
        if (other.type !== this.type) { 
            return false;
        }
        if (this.hash.keys().length !== other.hash.keys().length) { 
            return false;
        }

        var keys = this.hash.keys(), i;
        for (i = 0; i < keys.length; i++) {
            if (!(other.hash.containsKey(keys[i]) &&
                  baselib.equality.equals(this.hash.get(keys[i]),
                                          other.hash.get(keys[i]),
                                          aUnionFind))) {
                return false;
            }
        }
        return true;
    };

    WhalesongHashtable.prototype.hashCode = function(depth) {
        var k = getEqualHashCode(this.type);
        var keys = this.hash.keys(), i;
        for (i = 0; i < keys.length; i++) {
            k += hashMix(getEqualHashCode(this.hash.get(keys[i]), depth));
        }
        return hashMix(k);
    };


    WhalesongHashtable.prototype.get = function(key) {
        return this.hash.get(key);
    };

    WhalesongHashtable.prototype.put = function(key, value) {
        this.hash.put(key, value);
    };

    WhalesongHashtable.prototype.functionalPut = function(key, value) {
        return this.toImmutable().functionalPut(key, value);
    };

    WhalesongHashtable.prototype.remove = function(key) {
        this.hash.remove(key);
    };

    WhalesongHashtable.prototype.functionalRemove = function(key) {
        return this.toImmutable().functionalRemove(key);
    };

    WhalesongHashtable.prototype.containsKey = function(key) {
        return this.hash.containsKey(key);
    };

    WhalesongHashtable.prototype.isImmutable = function() {
        return false;
    };

    WhalesongHashtable.prototype.toImmutable = function() {
        var keycmp = makeComparator(this.hash_function, this.equality_function)
        var immutable = new WhalesongImmutableHashtable(
            this.type,
            this.hash_function,
            this.equality_function,
            LLRBTree.makeMap(keycmp));
        var keys = this.hash.keys();
        var i;
        for (i = 0; i < keys.length; i++) {
            immutable = immutable.functionalPut(keys[i], this.hash.get(keys[i]));
        }
        return immutable;
    };


    //////////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////
    // Whalesong's immutable hashtables are a thin wrapper around the
    // llrbtree class to make it printable and equatable.
    // llrbtree comes from: https://github.com/dyoo/js-llrbtree
    var WhalesongImmutableHashtable = function (type,
                                                hash_function,
                                                equality_function,
                                                map) {
        this.type = type;
        this.hash_function = hash_function;
        this.equality_function = equality_function;
        this.map = map;
    };

    WhalesongImmutableHashtable.prototype.size = function() {
        return this.map.items().length;
    };


    WhalesongImmutableHashtable.prototype.toWrittenString = function (cache) {
        var items = this.map.items();
        var ret = [], i;
        for (i = 0; i < items.length; i++) {
            var keyStr = baselib.format.toWrittenString(items[i][0], cache);
            var valStr = baselib.format.toWrittenString(items[i][1], cache);
            ret.push('(' + keyStr + ' . ' + valStr + ')');
        }
        return ('#' + this.type + '(' + ret.join(' ') + ')');
    };
    
    WhalesongImmutableHashtable.prototype.toDisplayedString = function (cache) {
        var items = this.map.keys();
        var ret = [], i;
        for (i = 0; i < items.length; i++) {
            var keyStr = baselib.format.toDisplayedString(items[i][0], cache);
            var valStr = baselib.format.toDisplayedString(items[i][1], cache);
            ret.push('(' + keyStr + ' . ' + valStr + ')');
        }
        return ('#' + this.type + '(' + ret.join(' ') + ')');
    };

    WhalesongImmutableHashtable.prototype.keys = function() {
        return this.map.keys();
    };

    WhalesongImmutableHashtable.prototype.values = function() {
        return this.map.values();
    };

    WhalesongImmutableHashtable.prototype.equals = function (other, aUnionFind) {
        if (!(other instanceof WhalesongImmutableHashtable)) {
            return false; 
        }
        if (other.type !== this.type) { 
            return false;
        }
        var litems = this.map.items();
        var ritems = other.map.items();

        if (litems.length !== ritems.length) { 
            return false;
        }
        var i;
        for (i = 0; i < litems.length; i++) {
            if (!(baselib.equality.equals(litems[i][0], ritems[i][0], aUnionFind))) {
                return false;
            }
            if (!(baselib.equality.equals(litems[i][1], ritems[i][1], aUnionFind))) {
                return false;
            }
        }
        return true;
    };

    WhalesongImmutableHashtable.prototype.hashCode = function(depth) {
        var k = getEqualHashCode(this.type);
        var items = this.map.items(), i;
        for (i = 0; i < items.length; i++) {
            k = getEqualHashCode(items[i][0], depth);
            k = hashMix(k);
            k = getEqualHashCode(items[i][1], depth);
            k = hashMix(k);
        }
        return hashMix(k);
    };


    WhalesongImmutableHashtable.prototype.get = function(key) {
        return this.map.get(key);
    };

    WhalesongImmutableHashtable.prototype.put = function(key, value) {
        throw new Error();
    };

    WhalesongImmutableHashtable.prototype.functionalPut = function(key, value) {
        return new WhalesongImmutableHashtable(this.type,
                                               this.hash_function,
                                               this.equality_function,
                                               this.map.put(key, value));
    };

    WhalesongImmutableHashtable.prototype.remove = function(key) {
        throw new Error();
    };

    WhalesongImmutableHashtable.prototype.functionalRemove = function(key) {
        return new WhalesongImmutableHashtable(this.type,
                                               this.hash_function,
                                               this.equality_function,
                                               this.map.remove(key));
    };

    WhalesongImmutableHashtable.prototype.containsKey = function(key) {
        return this.map.contains(key);
    };

    WhalesongImmutableHashtable.prototype.isImmutable = function() { 
        return true;
    };
    //////////////////////////////////////////////////////////////////////    



    var isHash = function (x) { 
        return (x instanceof WhalesongHashtable || x instanceof WhalesongImmutableHashtable);
    };

    var isHashEqual = function (x) { 
        return (x instanceof WhalesongHashtable || x instanceof WhalesongImmutableHashtable) && x.type === 'hash';
    };

    var isHashEqv = function (x) { 
        return (x instanceof WhalesongHashtable || x instanceof WhalesongImmutableHashtable) && x.type === 'hasheqv';
    };

    var isHashEq = function (x) { 
        return (x instanceof WhalesongHashtable || x instanceof WhalesongImmutableHashtable) && x.type === 'hasheq';
    };



    // Arbitrary magic number.  We have to cut off the hashing at some point.
    var MAX_HASH_DEPTH = 128;

    // Returns a JavaScript number.
    var getEqualHashCode = function (x, depth) {
        var i, t, k = 0;
        if (depth === void(0)) { depth = [0]; }

        if (depth[0] > MAX_HASH_DEPTH) { return 0; }

        if (baselib.numbers.isNumber(x)) {
            return hashMix(baselib.numbers.toFixnum(x));
        }

        if (baselib.strings.isString(x)) {
            t = x.toString();
            for (i = 0; i < t.length; i++) {
                k += t.charCodeAt(i);
                k = hashMix(k);
            }
            return k;
        }

        if (x === void(0) || x === null) {
            return 1;
        }

        if (typeof(x) === 'object' &&
            typeof(x.hashCode) === 'function') {
            depth[0] = depth[0] + 1;
            return x.hashCode(depth);
        }
        return 0;
    };


    // Does some weird math on k.  Grabbed from Racket's implementation of hashes.
    // References to: http://www.burtleburtle.net/bob/hash/doobs.html
    var hashMix = function(k) {
        k += (k << 10);
        k ^= (k >> 6);
        return k;
    };



    //////////////////////////////////////////////////////////////////////

    exports.getEqHashCode = getEqHashCode;
    exports.getEqualHashCode = getEqualHashCode;
    exports.getEqvHashCode = getEqvHashCode;

    exports.hashMix = hashMix;

    exports.makeEqHashCode = makeEqHashCode;
    exports.makeLowLevelEqHash = makeLowLevelEqHash;

    exports.makeEqHashtable = makeEqHashtable;
    exports.makeEqvHashtable = makeEqvHashtable;
    exports.makeEqualHashtable = makeEqualHashtable;

    exports.makeImmutableEqHashtable = makeImmutableEqHashtable;
    exports.makeImmutableEqvHashtable = makeImmutableEqvHashtable;
    exports.makeImmutableEqualHashtable = makeImmutableEqualHashtable;

    exports.isHash = isHash;

    exports.isHashEqual = isHashEqual;
    exports.isHashEqv = isHashEqv;
    exports.isHashEq = isHashEq;
}(window.plt.baselib, Hashtable));