Source: collections/Dictionary.js

const { Collection, CollectionEvent } = require('./Collection')
, { EqualityComparer } = require('./EqualityComparer')
, { Resolve } = require('../tools/Resolve')
, { Observable, fromEvent } = require('rxjs')
, symbolDictionaryGet = Symbol('dictionaryGet')
, symbolDictionarySet = Symbol('dictionarySet')
, symbolDictionaryDelete = Symbol('dictionaryDelete')
, util = require('util');



/**
 * @deprecated Use DictionaryMapBased instead. This Dictionary will be removed from version v3.0. Also, DictionaryMapBased will receive an alias of Dictionary.
 * @template T
 * @template TEq An extra template for the EqualityComparer, is it may be used for keys
 * and/or values simultaneously.
 * @extends {Collection<T>}
 * @author Sebastian Hönel <development@hoenel.net>
 */
class Dictionary extends Collection {
	/**
	 * Creates a new, empty Dictionary<T>.
	 * 
	 * @param {EqualityComparer<TEq>} [eqComparer] Optional. Defaults To EqualityComparer<TEq>.default.
	 */
	constructor(eqComparer = EqualityComparer.default) {
		super(eqComparer);

		/**
		 * @type {Object<String|Symbol, T>}
		 * @protected
		 */
		this._dict = Object.create(null);

		/**
		 * @type {Number}
		 * @protected
		 */
		this._size = 0;
	};

	/**
	 * Asserts that a key is suitable for use in an object.
	 * 
	 * @protected
	 * @param {String|Symbol} key The key to validate
	 * @throws {Error} If the key is not a string and not a symbol.
	 */
	_validateKey(key) {
		const type = typeof key;

		if (type !== 'string' && type !== 'symbol') {
			throw new Error(`The given key is not a string and not a Symbol: '${util.inspect(key)}'`);
		}
		if (key === '__proto__') {
			throw new Error(`You must not use a key with the name ${key}`);
		}
	};

	/**
	 * @param {String|Symbol} key
	 * @returns {T} The value of the removed item
	 */
	delete(key) {
		if (!this.hasKey(key)) {
			throw new Error(`The key ${key.toString()} is not present.`);
		}

		const item = this._dict[key];
		delete this._dict[key];
		this._size--;
		return item;
	};

	/**
	 * @deprecated Use ::delete(key) instead. This will be removed in v3.0.
	 * @param {String|Symbol} key The item's key.
	 * @returns {T} The removed item.
	 * @throws {Error} If the given key is not present.
	 */
	remove(key) {
		return this.delete(key);
	};

	/**
	 * Sets the item with the given key. If the key exists previously, its
	 * value is overwritten.
	 * 
	 * @param {String|Symbol} key 
	 * @param {T} val
	 * @returns {this}
	 */
	set(key, val) {
		const hadKey = this.hasKey(key);
		this._dict[key] = val;
		if (!hadKey) {
			this._size++;
		}
		return this;
	};

	/**
	 * @param {String|Symbol} key The item's key.
	 * @returns {T} The item
	 * @throws {Error} If the key is not present.
	 */
	get(key) {
		if (this.hasKey(key)) {
			return this._dict[key];
		}
		throw new Error(`The key '${key.toString()}' is not present.`);
	};

	/**
	 * @param {String|Symbol} key The item's key.
	 * @param {EqualityComparer<TEq>} [eqComparer] an optional EqualityComparer to use. If not provided, will use the Collection's EqualityComparer.
	 * @returns {Boolean} True, if the given key is present.
	 */
	hasKeyEq(key, eqComparer = null) {
		this._validateKey(key);
		eqComparer = eqComparer instanceof EqualityComparer ? eqComparer : this._eqComparer;
		const allKeys = Array.from(this.keys())
		, idx = allKeys.findIndex(value => eqComparer.equals(value, key));
		return idx >= 0;
	};

	/**
	 * Does not support an @see {EqualityComparer<TEq>} and can therefore facilitate faster O(1) lookups.
	 * 
	 * @param {String|Symbol} key The item's key.
	 * @returns {Boolean} True, if the given key is present.
	 */
	hasKey(key) {
		this._validateKey(key);
		return key in this._dict;
	};

	/**
	 * @param {T} item The item to check for
	 * @param {EqualityComparer<TEq>} eqComparer An optional EqualityComparer to use. If not provided,
	 * will use the Collection's EqualityComparer.
	 * @returns {Boolean}
	 */
	has(item, eqComparer = null) {
		eqComparer = eqComparer instanceof EqualityComparer ? eqComparer : this._eqComparer;
		for (const value of this.values()) {
			if (eqComparer.equals(value, item)) {
				return true;
			}
		}
		return false;
	};

	/**
	 * @type {Number}
	 */
	get size() {
		return this._size;
	};

	/**
	 * Removes all items from this dictionary.
	 * 
	 * @override
	 * @inheritdoc
	 * @returns {this}
	 */
	clear() {
		super.clear();
		this._dict = Object.create(null);
		this._size = 0;
		return this;
	};

	/**
	 * @returns {IterableIterator<String|Symbol>}
	 */
	*keys() {
		for (const key of Object.keys(this._dict)) {
			yield key;
		}
		for (const key of Object.getOwnPropertySymbols(this._dict)) {
			yield key;
		}
	};

	/**
	 * @returns {IterableIterator<T>}
	 */
	*values() {
		for (const key of this.keys()) {
			yield this._dict[key];
		}
	};

	/**
	 * @protected
	 * @param {Boolean} reverse True, if the entries should be returned in reverse order.
	 * @returns {IterableIterator<Object<String|Symbol, T>>}
	 */
	*_entries(reverse = false) {
		let keys = Array.from(this.keys());
		if (reverse) {
			keys = keys.reverse();
		}
		
		for (const key of keys) {
			const obj = {};
			obj[key] = this._dict[key];
			yield obj;
		}
	};

	/**
	 * Returns all entries of this dictionary. Note that the order is deterministic and
	 * that therefore returning the entries in reverse order may also be of use.
	 * @link {https://2ality.com/2015/10/property-traversal-order-es6.html#traversing-the-own-keys-of-an-object}
	 * 
	 * @returns {IterableIterator<Object<String|Symbol, T>>}
	 */
	*entries() {
		for (const entry of this._entries()) {
			yield entry;
		}
	};

	/**
	 * @returns {IterableIterator<Object<String|Symbol, T>>}
	 */
	*entriesReversed() {
		for (const entry of this._entries(true)) {
			yield entry;
		}
	};
};



/**
 * @template TKey
 * @template TVal
 * @extends {Collection<Array<Tkey|TVal>>}
 * @author Sebastian Hönel <development@hoenel.net>
 */
class DictionaryMapBased extends Collection {
	/**
	 * @param {EqualityComparer<TVal>} [eqComparer]
	 */
	constructor(eqComparer = EqualityComparer.default) {
		super(eqComparer);

		/**
		 * @type {Map<TKey, TVal>}
		 * @protected
		 */
		this._map = new Map();

		/** @type {Observable<CollectionEvent<[TKey, TVal]>>} */
		this.observableSet = Object.freeze(fromEvent(this, symbolDictionarySet));
		/** @type {Observable<CollectionEvent<[TKey, TVal]>>} */
		this.observableGet = Object.freeze(fromEvent(this, symbolDictionaryGet));
		/** @type {Observable<CollectionEvent<[TKey, TVal]>>} */
		this.observableDelete = Object.freeze(fromEvent(this, symbolDictionaryDelete));
	};

	/**
	 * Inverts this dictionary by using its values as keys, and storing
	 * for each value a list of keys that previously pointed to that
	 * value. Therefore, the values in the inverted dictionary are lists.
	 * E.g., the dictionary {A=>5, B=>5, C=>2} inverts to the dictionary
	 * {5=>[A,B], 2=>[C]}. Therefore, you also cannot invert an inverted
	 * dictionary and obtain a dictionary equivalent to the original.
	 * 
	 * @param {EqualityComparer<TVal>} [eqComparer] Optional. Uses the
	 * default EqualityComparer if not given. You may also pass in this
	 * dictionary's comparer. It may make sense to use a separate comparer,
	 * as the values may be of different type than the keys.
	 * @returns {DictionaryMapBased<TVal, Array<TKey>>}
	 */
	invert(eqComparer = EqualityComparer.default) {
		/** @type {DictionaryMapBased<TVal, Array<TKey>>} */
		const dict = new DictionaryMapBased(eqComparer);

		for (const entry of this.entries()) {
			const key = entry[0], val = entry[1];
			if (dict.has(val)) {
				dict.get(val).push(key);
			} else {
				dict.set(val, [key]);
			}
		}

		return dict;
	};

	/**
	 * @override
	 * @inheritdoc
	 * @returns {this}
	 */
	clear() {
		super.clear();
		this._map.clear();
		return this;
	};

	/**
	 * @param {TKey} key The key to check for
	 * @returns {Boolean}
	 */
	has(key) {
		return this._map.has(key);
	};

	/**
	 * @returns {IterableIterator<[TKey, TVal]>}
	 */
	*entries() {
		yield* this._map.entries();
	};

	/**
	 * @returns {IterableIterator<TVal>}
	 */
	*values() {
		yield* this._map.values();
	};

	/**
	 * @returns {IterableIterator<TKey>}
	 */
	*keys() {
		yield* this._map.keys();
	}

	/**
	 * @returns {IterableIterator<[TKey, TVal]>}
	 */
	*entriesReversed() {
		const entries = Array.from(this.entries());

		for (const entry of entries.reverse()) {
			yield entry;
		}
	};

	/**
	 * @param {TVal} value The value to check for
	 * @param {EqualityComparer<TVal>} [eqComparer] An optional @see {EqualityComparer}. Uses the internal comparer if not given.
	 * @returns {Boolean}
	 */
	hasValue(value, eqComparer = null) {
		eqComparer = eqComparer instanceof EqualityComparer ? eqComparer : this._eqComparer;
		
		for (const val of this.values()) { // Values are already unwrapped
			if (eqComparer.equals(val, value)) {
				return true;
			}
		}

		return false;
	};

	/**
	 * @type {Number}
	 */
	get size() {
		return this._map.size;
	};

	/**
	 * @param {TKey} key The key of the value to retrieve.
	 * @throws {Error} If the key is not found.
	 * @returns {TVal}
	 */
	get(key) {
		if (!this.has(key)) {
			throw new Error(`The key '${key}' is not present.`);
		}
		const value = this.__unwrapValue(this._map.get(key));
		this.emit(symbolDictionaryGet, new CollectionEvent([key, value]));
		return value;
	};

	/**
	 * @param {TKey} key The of the entry to delete.
	 * @returns {TVal} The deleted value
	 * @throws {Error} If the key cannot be found or cannot be deleted.
	 */
	delete(key) {
		const value = this.get(key); // throws if does not exist
		this._map.delete(key); // returns true because key exists
		this.emit(symbolDictionaryDelete, new CollectionEvent([key, value]));
		return value; // this::get() already unwraps
	};

	/**
	 * @param {consumerProducer4ArgHandler<TVal, TKey, Number, Dictionary<TKey, TVal>, any>} callbackFn receives the value, the key, the index, and the dictionary
	 * @param {*} [thisArg] Optional. Defaults to undefined. 
	 */
	forEach(callbackFn, thisArg = void 0) {
		if (!Resolve.isFunction(callbackFn)) {
			throw new Error(`The value given as callback function is not a function: '${util.inspect(callbackFn)}'`);
		}

		const that = this
		, withKey = callbackFn.length > 1
		, withIdx = callbackFn.length > 2
		, withDict = callbackFn.length > 3;

		let idx = 0;
		this._map.forEach((val, key, map) => {
			const args = [this.__unwrapValue(val)];
			if (withKey) { args.push(this.__unwrapKey(key)); }
			if (withIdx) { args.push(idx++); }
			if (withDict) { args.push(that); }
			callbackFn.apply(thisArg, args);
		});
		
		return this;
	};

	/**
	 * Used for unwrapping values. This is useful for collections that wrap
	 * the key in some way. The DictionaryMap's methods use this method, so
	 * that deriving subclasses only need to override this method.
	 * 
	 * @see __unwrapValue
	 * @private
	 * @param {TKey} key The key to unwrap
	 * @returns {TKey} In this base class, this is an identity function.
	 * Sub-classes may override it.
	 */
	__unwrapKey(key) {
		return key;
	};

	/**
	 * Used for unwrapping values. This is useful for collections that wrap
	 * the value in some way. The DictionaryMap's methods use this method,
	 * so that deriving subclasses only need to override this method.
	 * 
	 * @see __unwrapKey
	 * @private
	 * @param {TVal} value The value to unwrap
	 * @returns {TVal} In this base class, this is an identity function.
	 * Sub-classes may override it.
	 */
	__unwrapValue(value) {
		return value;
	};

	/**
	 * Sets (upserts) a value with the given key.
	 * 
	 * @param {TKey} key The key of the value.
	 * @param {TVal} value The value associated with the key.
	 * @returns {this}
	 */
	set(key, value) {
		this._map.set(key, value);
		this.emit(symbolDictionarySet, new CollectionEvent([key, value]));
		return this;
	};
};


module.exports = Object.freeze({
	Dictionary,
	DictionaryMapBased,
	symbolDictionaryGet,
	symbolDictionarySet,
	symbolDictionaryDelete
});