const CompareUtil = require('./compare-util');

const DEFAULT_CAPACITY = 101;
const DEFAULT_LOAD_FACTOR = 0.75;

function LinkedSetEntry(key, next = null) {
    this.key = key;
    this.next = next;
    this.link_next = null;
    this.link_prev = null;
}

LinkedSetEntry.prototype.clone = function () {
    return new LinkedSetEntry(this.key, this.next ? this.next.clone() : null);
};

LinkedSetEntry.prototype.equals = function (other) {
    return other instanceof LinkedSetEntry && CompareUtil.equals(this.key, other.key);
};

LinkedSetEntry.prototype.hashCode = function () {
    return this.key.hashCode();
};

LinkedSetEntry.prototype.toString = function () {
    return this.key.toString();
};

function LinkedSet(initCapacity = DEFAULT_CAPACITY, loadFactor = DEFAULT_LOAD_FACTOR) {
    if (initCapacity < 0) throw new Error(`Capacity Error: ${initCapacity}`);
    if (loadFactor <= 0) throw new Error(`Load Count Error: ${loadFactor}`);
    if (initCapacity === 0) initCapacity = 1;

    this.loadFactor = loadFactor;
    this.table = new Array(initCapacity).fill(null);
    this.header = new LinkedSetEntry(null);
    this.header.link_next = this.header.link_prev = this.header;
    this.count = 0;
    this.threshold = Math.floor(initCapacity * loadFactor);
    this.max = 0;
}

LinkedSet.prototype.size = function () {
    return this.count;
};

LinkedSet.prototype.getArray = function (out) {
    if (out.length !== this.size()) {
        out = new Array(this.size());
    }
    let en = this.elements();
    for (let i = 0; en.hasMoreElements(); i++) {
        out[i] = en.nextElement();
    }
    return out;
};

LinkedSet.prototype.elements = function () {
    return new LinkedSetEnumerator(this.header);
};

LinkedSet.prototype.contains = function (key) {
    if (key === null) return false;
    let index = this.hash(key) % this.table.length;
    for (let e = this.table[index]; e !== null; e = e.next) {
        if (CompareUtil.equals(e.key, key)) {
            return true;
        }
    }
    return false;
};

LinkedSet.prototype.getFirst = function () {
    return this.header.link_next.key;
};

LinkedSet.prototype.getLast = function () {
    return this.header.link_prev.key;
};

LinkedSet.prototype.hash = function (key) {
    return (key.hashCode() & 0x7FFFFFFF);
};

LinkedSet.prototype.rehash = function () {
    let oldCapacity = this.table.length;
    let oldMap = this.table;
    let newCapacity = oldCapacity * 2 + 1;
    let newMap = new Array(newCapacity).fill(null);
    this.threshold = Math.floor(newCapacity * this.loadFactor);
    this.table = newMap;
    for (let i = oldCapacity; i-- > 0;) {
        for (let old = oldMap[i]; old !== null;) {
            let e = old;
            old = old.next;
            let index = this.hash(e.key) % newCapacity;
            e.next = newMap[index];
            newMap[index] = e;
        }
    }
};

LinkedSet.prototype.setMax = function (max) {
    this.max = max;
    return this;
};

LinkedSet.prototype.put = function (key) {
    return this._put(key, LinkedSet.MODE.LAST);
};

LinkedSet.prototype.putLast = function (key) {
    return this._put(key, LinkedSet.MODE.FORCE_LAST);
};

LinkedSet.prototype.putFirst = function (key) {
    return this._put(key, LinkedSet.MODE.FORCE_FIRST);
};

LinkedSet.prototype._put = function (key, mode) {
    if (key === null) return null;
    let index = this.hash(key) % this.table.length;
    for (let e = this.table[index]; e !== null; e = e.next) {
        if (CompareUtil.equals(e.key, key)) {
            switch (mode) {
                case LinkedSet.MODE.FORCE_FIRST:
                    if (this.header.link_next !== e) {
                        this.unchain(e);
                        this.chain(this.header, this.header.link_next, e);
                    }
                    break;
                case LinkedSet.MODE.FORCE_LAST:
                    if (this.header.link_prev !== e) {
                        this.unchain(e);
                        this.chain(this.header.link_prev, this.header, e);
                    }
                    break;
            }
            return e.key;
        }
    }

    if (this.max > 0) {
        switch (mode) {
            case LinkedSet.MODE.FORCE_FIRST:
            case LinkedSet.MODE.FIRST:
                while (this.count >= this.max) {
                    let v = this.header.link_prev.key;
                    this.remove(v);
                    this.overflowed(v);
                }
                break;
            case LinkedSet.MODE.FORCE_LAST:
            case LinkedSet.MODE.LAST:
                while (this.count >= this.max) {
                    let v = this.header.link_next.key;
                    this.remove(v);
                    this.overflowed(v);
                }
                break;
        }
    }

    if (this.count >= this.threshold) {
        this.rehash();
        index = this.hash(key) % this.table.length;
    }
    let e = new LinkedSetEntry(key, this.table[index]);
    this.table[index] = e;

    switch (mode) {
        case LinkedSet.MODE.FORCE_FIRST:
        case LinkedSet.MODE.FIRST:
            this.chain(this.header, this.header.link_next, e);
            break;
        case LinkedSet.MODE.FORCE_LAST:
        case LinkedSet.MODE.LAST:
            this.chain(this.header.link_prev, this.header, e);
            break;
    }

    this.count++;
    return key;
};

LinkedSet.prototype.overflowed = function (value) {
    // Custom logic for handling overflow can be added here
};

LinkedSet.prototype.remove = function (key) {
    if (key === null) return false;

    let index = this.hash(key) % this.table.length;
    for (let e = this.table[index], prev = null; e !== null; prev = e, e = e.next) {
        if (CompareUtil.equals(e.key, key)) {
            if (prev !== null) {
                prev.next = e.next;
            } else {
                this.table[index] = e.next;
            }
            this.count--;
            this.unchain(e);
            return true;
        }
    }
    return false;
};

LinkedSet.prototype.removeFirst = function () {
    if (this.isEmpty()) return false;
    return this.remove(this.header.link_next.key);
};

LinkedSet.prototype.removeLast = function () {
    if (this.isEmpty()) return false;
    return this.remove(this.header.link_prev.key);
};

LinkedSet.prototype.isEmpty = function () {
    return this.size() === 0;
};

LinkedSet.prototype.clear = function () {
    this.table.fill(null);
    this.header.link_next = this.header.link_prev = this.header;
    this.count = 0;
};

LinkedSet.prototype.toString = function () {
    let buf = '{';
    let it = this.elements();
    while (it.hasMoreElements()) {
        if (buf.length > 1) buf += ',';
        buf += it.nextElement();
    }
    buf += '}';
    return buf;
};

LinkedSet.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;
};

LinkedSet.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;
};

function LinkedSetEnumerator(header) {
    this.entry = header.link_next;
    this.header = header;
}

LinkedSetEnumerator.prototype.hasMoreElements = function () {
    return this.header !== this.entry && this.entry !== null;
};

LinkedSetEnumerator.prototype.nextElement = function () {
    if (this.hasMoreElements()) {
        let e = this.entry;
        this.entry = e.link_next;
        return e.key;
    } else {
        return null;
    }
};

LinkedSet.MODE = {
    LAST: 0,
    FORCE_LAST: 1,
    FORCE_FIRST: 2,
};

module.exports = LinkedSet;
