/**
 * Copyright 2016 the WHATAP project authors. All rights reserved.
 * Use of this source code is governed by a license that
 * can be found in the LICENSE file.
 */


var HashUtil = require('./hashutil');

String.prototype._hash_=0;

function isNone(key){
    return key == null ;
}
function LinkedEntry(key, value, next) {
    this.key = key;
    this.value = value;
    this.next = next;      /*LinkedEntry*/
    this.link_next = null; /*LinkedEntry*/
    this.link_prev = null; /*LinkedEntry*/
}
LinkedEntry.prototype.getKey = function () {
    return this.key;
};
LinkedEntry.prototype.getValue = function () {
    return this.value;
};
LinkedEntry.prototype.setValue = function (value) {
    var oldValue = this.value;
    this.value = value;
    return oldValue;
};

LinkedEntry.prototype.toString = function () {
    if(this.value.toString){
        return this.key +"="+this.value.toString();
    }
    return this.key + "=" + this.value;
};

function Enumer(map, type) {
    this.map = map;
    this.entry = map.header.link_next;
    this.lastEnt = null;
    this.type = type ;
}
Enumer.prototype.hasMoreElements = function () {
    return this.map.header != this.entry && this.entry != null;
};
Enumer.prototype.nextElement = function () {
    if(this.hasMoreElements()) {
        var e = this.lastEnt = this.entry;
        this.entry = e.link_next;
        switch (this.type) {
            case TYPE.KEYS :
                return e.key;
            case TYPE.VALUES :
                return e.value;
            default :
                return e;
        }
    }
    throw new Error("No more next");
};

const DEFAULT_CAPACITY = 101;
const DEFAULT_LOAD_FACTOR = 0.75;
const TYPE = {
    KEYS    : 1,
    VALUES  : 2,
    ENTRIES : 3
};
const MODE = {
    FORCE_FIRST : 1,
    FORCE_LAST  : 2,
    FIRST       : 3,
    LAST        : 4
};

function LinkedMap(initCapacity, loadFactor) {
    this.initCapacity = initCapacity || DEFAULT_CAPACITY;
    this.loadFactor = loadFactor || DEFAULT_LOAD_FACTOR;
    this.threshold = parseInt(this.initCapacity * this.loadFactor);

    this.table = new Array(this.initCapacity); /*LinkedEntry*/
    this.header = new LinkedEntry(null, null, null); /*LinkedEntry*/
    this.header.link_next = this.header.link_prev = this.header;
    this.count = 0;
    this.max = 0;
}
LinkedMap.prototype.none = function () {
    return null;
}
LinkedMap.prototype.size = function () {
    return this.count;
};
LinkedMap.prototype.keys = function () {
    return new Enumer(this, TYPE.KEYS);
};
LinkedMap.prototype.values = function () {
    return new Enumer(this, TYPE.VALUES);
};
LinkedMap.prototype.entries = function () {
    return new Enumer(this, TYPE.ENTRIES);
};
LinkedMap.prototype.containsValue = function (value) {
    if(isNone(value)) {
        return false;
    }
    var tab = this.table;
    var i = tab.length;
    while(i-- > 0) {
        for(var e = tab[i]; e != null; e = e.next) {
            if(e.value === value) {
                return true;
            }
        }
    }
    return false;
};
LinkedMap.prototype.containsKey = function (key) {
    if(isNone(key)) {
        return false;
    }
    var tab = this.table;
    var index =  Math.abs(this.hash(key)) % tab.length;

    for(var e = tab[index]; e != null; e = e.next) {
        if(this.equals(e.key, key)) {
            return true;
        }
    }
    return false;
};
LinkedMap.prototype.get = function (key) {
    if(isNone(key)) {
        return this.none();
    }

    var tab = this.table;
    var index = Math.abs(this.hash(key)) % tab.length;

    for(var e = tab[index]; e != null; e = e.next) {
        if(this.equals(e.key, key)) {
            return e.value;
        }
    }
    return this.none();
};
LinkedMap.prototype.getLastValue = function () {
    if(this.isEmpty()) {
        return this.none();
    }
    return this.header.link_prev.value;
};

LinkedMap.prototype.strHash = function(str) {
    var hash = 0, i, chr, len;
    if (str.length === 0) return hash;
    for (i = 0, len = str.length; i < len; i++) {
        chr   = str.charCodeAt(i);
        hash  = ((hash << 5) - hash) + chr;
        hash |= 0; // Convert to 32bit integer
    }
    return hash;
}
LinkedMap.prototype.hash = function (key) {
    if(key._hash_){
        return key._hash_;
    }
    if(key.hashCode) {
        key._hash_ = Math.abs(key.hashCode());
        return key._hash_;
    }
    key._hash_ = Math.abs(this.strHash(key.toString()));
    return key._hash_;
};
LinkedMap.prototype.equals = function (key1, key2) {
    if(key1.equals){
        return key1.equals(key2);
    }
    return key1===key2;
};
//원칙은 sum에서 데이터를 합해야 하지만... 객체인경우에는 update로 처리한다. 
LinkedMap.prototype.sum = function (value1, value2) {
    return value2;
};

LinkedMap.prototype.rehash = function () {
    var oldCapacity = this.table.length;
    var oldMap = this.table;
    var newCapacity = oldCapacity * 2 + 1;
    var newMap = new Array(newCapacity);
    this.threshold = parseInt(newCapacity * this.loadFactor);
    this.table = newMap;

    for(var i=oldCapacity ; i-- > 0;) {
        for(var old = oldMap[i]; old!=null;) {
            var e = old;
            old = old.next;
            var key = e.key;
            var index = Math.abs(this.hash(key)) % newCapacity;
            e.next = newMap[index];
            newMap[index] = e;
        }
    }

};
LinkedMap.prototype.isEmpty = function () {
    return this.size() === 0;
};
LinkedMap.prototype.setMax = function (max) {
    this.max = max;
    return this;
};
LinkedMap.prototype.isFull = function () {
    return this.max > 0 && this.max <= this.count;
};
LinkedMap.prototype.put = function (key, value) {
    return this._put(key, value, MODE.LAST);
};
LinkedMap.prototype.putLast = function (key, value) {
    return this._put(key, value, MODE.FORCE_LAST);
};
LinkedMap.prototype.putFirst = function (key, value) {
    return this._put(key, value, MODE.FORCE_FIRST);
};
LinkedMap.prototype._put = function (key, value, m) {
    if(isNone(key)){
        return this.none();
    }
    var tab = this.table;
    var index = Math.abs(this.hash(key)) % tab.length;
    for(var e = tab[index]; e != null; e = e.next) {
        if(this.equals(e.key, key)) {
            var old = e.value;
            e.value = value;
            switch(m) {
                case MODE.FORCE_FIRST:
                    if(this.header.link_next != e) {
                        this.unchain(e);
                        this.chain(this.header, this.header.link_next, e);
                    }
                    break;
                case MODE.FORCE_LAST:
                    if(this.header.link_prev != e) {
                        this.unchain(e);
                        this.chain(this.header.link_prev, this.header, e);
                    }
                    break;
            }
            return old;
        }
    }
    if( this.max > 0 ) {
        switch (m) {
            case MODE.FORCE_FIRST:
            case MODE.FIRST:
                while( this.count >= this.max ) {
                    var k = this.header.link_prev.key;
                    var v = this.remove(k);
                    this.overflowed(k, v);
                }
                break;
            case MODE.FORCE_LAST:
            case MODE.LAST:
                while( this.count >= this.max ) {
                    var k = this.header.link_next.key;
                    var v = this.remove(k);
                    this.overflowed(k, v);
                }
                break;
        }
    }
    if(this.count >= this.threshold) {
        this.rehash();
        tab = this.table;
        index = Math.abs(this.hash(key)) % tab.length;
    }

    var e = new LinkedEntry(key ,value, tab[index]);
    tab[index] = e;

    switch (m) {
        case MODE.FORCE_FIRST:
        case MODE.FIRST:
            this.chain(this.header, this.header.link_next, e);
            break;
        case MODE.FORCE_LAST:
        case MODE.LAST:
            this.chain(this.header.link_prev, this.header, e);
            break;
    }
    this.count++;
    return this.none();
};
LinkedMap.prototype.add = function (key, value) {
    return this._add(key, value, MODE.LAST);
};
LinkedMap.prototype.addLast = function (key, value) {
    return this._add(key, value, MODE.FORCE_LAST);
};
LinkedMap.prototype.addFirst = function (key, value) {
    return this._add(key, value, MODE.FORCE_FIRST);
};
LinkedMap.prototype._add = function (key, value, m, noover) {
    if(isNone(key)) {
        return this.none();
    }
    if(noover && this.max > 0 && this.size() > this.max) {
        return this.none();
    }
    var tab = this.table;
    var index = Math.abs(this.hash(key)) % tab.length;
    for(var e = tab[index]; e != null; e = e.next) {
        if(this.equals(e.key, key)) {
            e.value = this.sum(e.value,value);
            switch(m) {
                case MODE.FORCE_FIRST:
                    if(this.header.link_next != e) {
                        this.unchain(e);
                        this.chain(this.header, this.header.link_next, e);
                    }
                    break;
                case MODE.FORCE_LAST:
                    if(this.header.link_prev != e) {
                        this.unchain(e);
                        this.chain(this.header.link_prev, this.header, e);
                    }
                    break;
            }
            return e.value;
        }
    }
    if( this.max > 0 ) {
        switch (m) {
            case MODE.FORCE_FIRST:
            case MODE.FIRST:
                while( this.count >= this.max ) {
                    var k = this.header.link_prev.key;
                    var v = this.remove(k);
                    this.overflowed(k, v);
                }
                break;
            case MODE.FORCE_LAST:
            case MODE.LAST:
                while( this.count >= this.max ) {
                    var k = this.header.link_next.key;
                    var v = this.remove(k);
                    this.overflowed(k, v);
                }
                break;
        }
    }
    if(this.count >= this.threshold) {
        this.rehash();
        tab = this.table;
        index = Math.abs(this.hash(key)) % tab.length;
    }

    var e = new LinkedEntry(key ,value, tab[index]);
    tab[index] = e;

    switch (m) {
        case MODE.FORCE_FIRST:
        case MODE.FIRST:
            this.chain(this.header, this.header.link_next, e);
            break;
        case MODE.FORCE_LAST:
        case MODE.LAST:
            this.chain(this.header.link_prev, this.header, e);
            break;
    }
    this.count++;
    return this.none();
};

LinkedMap.prototype.addNoOver = function (key, value) {
    return this._addNoOver(key, value, MODE.LAST, true);
};
LinkedMap.prototype._addNoOver = function (key, value, m, noover) {
    if(isNone(key)) {
        return this.none();
    }
    
    var tab = this.table;
    var index = Math.abs(this.hash(key)) % tab.length;
    for(var e = tab[index]; e != null; e = e.next) {
        if(this.equals(e.key, key)) {
            e.value = this.sum(e.value,value);
            switch(m) {
                case MODE.FORCE_FIRST:
                    if(this.header.link_next != e) {
                        this.unchain(e);
                        this.chain(this.header, this.header.link_next, e);
                    }
                    break;
                case MODE.FORCE_LAST:
                    if(this.header.link_prev != e) {
                        this.unchain(e);
                        this.chain(this.header.link_prev, this.header, e);
                    }
                    break;
            }
            return e.value;
        }
    }
    //키 갯수가 max에 도달하면 더이상 중가 시키지 않는다. 
    if( this.max > 0 && this.count >= this.max ) {
        return null;
    }
    if(this.count >= this.threshold) {
        this.rehash();
        tab = this.table;
        index = Math.abs(this.hash(key)) % tab.length;
    }

    var e = new LinkedEntry(key ,value, tab[index]);
    tab[index] = e;

    switch (m) {
        case MODE.FORCE_FIRST:
        case MODE.FIRST:
            this.chain(this.header, this.header.link_next, e);
            break;
        case MODE.FORCE_LAST:
        case MODE.LAST:
            this.chain(this.header.link_prev, this.header, e);
            break;
    }
    this.count++;
    return this.none();
};

LinkedMap.prototype.overflowed = function (key, value) {
};
LinkedMap.prototype.create = function (key) {
    throw new Error('not implemented create()');
};
LinkedMap.prototype.intern = function (key) {
    return this._intern(key, MODE.LAST);
};
LinkedMap.prototype._intern = function (key, m) {
    var tab = this.table;
    var index = Math.abs(this.hash(key)) % tab.length;

    for(var e = tab[index]; e != null; e = e.next) {
        if(this.equals(e.key, key)) {
            var old = e.value;
            switch(m) {
                case MODE.FORCE_FIRST:
                    if(this.header.link_next != e) {
                        this.unchain(e);
                        this.chain(this.header, this.header.link_next, e);
                    }
                    break;
                case MODE.FORCE_LAST:
                    if(this.header.link_prev != e) {
                        this.unchain(e);
                        this.chain(this.header.link_prev, this.header, e);
                    }
                    break;
            }
            return old;
        }
    }

    var value = this.create(key);

    if(isNone(value)) {
        return this.none();
    }
    if(this.max > 0) {
        switch (m) {
            case MODE.FORCE_FIRST:
            case MODE.FIRST:
                while (this.count >= this.max) {
                    var k = this.header.link_prev.key;
                    var v = this.remove(k);
                    this.overflowed(k, v);
                }
                break;
            case MODE.FORCE_LAST:
            case MODE.LAST:
                while (this.count >= this.max) {
                    var k = this.header.link_next.key;
                    var v = this.remove(k);
                    this.overflowed(k, v);
                }
                break;
        }
    }

    if(this.count >= this.threshold) {
        this.rehash();
        tab = this.table;
        index = Math.abs(this.hash(key)) % tab.length;
    }

    var e = new LinkedEntry(key, value, tab[index]);
    tab[index] = e;
    
    switch (m) {
        case MODE.FORCE_FIRST:
        case MODE.FIRST:
            this.chain(this.header, this.header.link_next, e);
            break;
        case MODE.FORCE_LAST:
        case MODE.LAST:
            this.chain(this.header.link_prev, this.header, e);
            break;
    }
    this.count++;
    return value;
};
LinkedMap.prototype.remove = function (key) {
    if(isNone(key)) {
        return this.none();
    }
    var tab = this.table;
    var index = Math.abs(this.hash(key)) % tab.length;
    for(var e = tab[index], prev = null; e != null; prev = e, e = e.next) {
        if(this.equals(e.key, key)) {
            if(prev != null) {
                prev.next = e.next;
            } else {
                tab[index] = e.next;
            }
            this.count--;
            var oldValue = e.value;
            e.value = null;
            this.unchain(e);
            return oldValue;
        }
    }
    return this.none();
};
LinkedMap.prototype.removeFirst = function () {
    if(this.isEmpty()) {
        return this.none();
    }
    return this.remove(this.header.link_next.key);
};
LinkedMap.prototype.removeLast = function () {
    if(this.isEmpty()) {
        return this.none();
    }
    return this.remove(this.header.link_prev.key);
};
LinkedMap.prototype.clear = function () {
    var tab = this.table;
    for(var index = tab.length; --index >=0;) {
        tab[index] = null;
    }
    this.header.link_next = this.header.link_prev = this.header;
    this.count = 0;
};
LinkedMap.prototype.chain = function (link_prev, link_next, e) {
    e.link_prev = link_prev;
    e.link_next = link_next;
    link_prev.link_next = e;
    link_next.link_prev = e;
};
LinkedMap.prototype.unchain = function (e) {
    e.link_prev.link_next = e.link_next;
    e.link_next.link_prev = e.link_prev;
    e.link_prev = null;
    e.link_next = null;
};
LinkedMap.prototype.putAll = function (other) {
    if((other instanceof LinkedMap) == false) {
        return this.none();
    }
    var it = other.entries();
    while(it.hasMoreElements()) {
        var e = it.nextElement();
        this.put(e.getKey(), e.getValue());
    }
};
LinkedMap.prototype.toString = function () {
    var str = '';
    var it = this.entries();
    str += '{';
    for(var i=0; it.hasMoreElements(); i++) {
        var e = it.nextElement();
        if(i > 0) {
            str += ", ";
        }
        str += JSON.stringify(e.getKey());
        str += "=";
        str += JSON.stringify(e.getValue());
    }
    str += "}";
    return str;
};

module.exports = LinkedMap;
module.exports.MODE = MODE;