import CollectionTools from './collectionTools';
import IMapPair from './interfaces/iMapPair';
import IMap from './interfaces/iMap';


export class MapList<K, V> implements IMap<K, V> {
    /**
     * Creates an empty map.
     * @class <p>Dictionaries map keys to values; each key can map to at most one value.
     * This implementation accepts any kind of objects as keys.</p>
     *
     * <p>If the keys are custom objects a function which converts keys to unique
     * strings must be provided. Example:</p>
     * <pre>
     * function petToString(pet) {
     *  return pet.key;
     * }
     * </pre>
     * @constructor
     * @param {function(Object):string=} _toStringFunction optional function used
     * to convert keys to strings. If the keys aren't strings or if _toStringing()
     * is not appropriate, a custom function which receives a key and returns a
     * unique string must be provided.
     */
    constructor(_toStringFunction?:(key:K) => string) {
        this.table = {};
        this.p_length = 0;
        this._toString = _toStringFunction || CollectionTools.defaultToString;
    }


    /**
     * Object holding the key-value pairs.
     * [key: K] will not work since indices can only be strings in javascript and typescript enforces this.
     */
    private table:{ [key:string]:IMapPair<K, V> };


    /**
     * Number of elements in the list.
     */
    /**
     * Returns the number of keys in this map.
     * @return {number} the number of key-value mappings in this map.
     */
    get size():number { return this.p_length; }
    protected p_length:number;


    /**
     * Function used to convert keys to strings.
     */
    private _toString:(key:K) => string;


    /**
     * Returns the value to which this map maps the specified key.
     * Returns undefined if this map has no mapping for this key.
     * @param {Object} key key whose associated value is to be returned.
     * @return {*} the value to which this map maps the specified key or undefined if the map has no mapping for this key.
     */
    get(key:K):V {
        let pair:IMapPair<K, V> = this.table[this._toString(key)];
        if (CollectionTools.isUndefined(pair)) return undefined;
        return pair.value;
    }


    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for this key, the old
     * value is replaced by the specified value.
     * @param {Object} key key with which the specified value is to be associated.
     * @param {Object} value value to be associated with the specified key.
     * @return {*} previous value associated with the specified key, or undefined if there was no mapping for the key or if the key/value are undefined.
     */
    set(key:K, value:V):V {

        if (CollectionTools.isUndefined(key) || CollectionTools.isUndefined(value)) return undefined;

        let ret:V;
        let k = this._toString(key);
        let previousElement:IMapPair<K, V> = this.table[k];
        if (CollectionTools.isUndefined(previousElement)) {
            this.p_length++;
            ret = undefined;
        } else ret = previousElement.value;
        this.table[k] = {
            key: key,
            value: value
        };
        return ret;
    }


    /**
     * Removes the mapping for this key from this map if it is present.
     * @param {Object} key key whose mapping is to be deleted from the map.
     * @return {*} value associated with specified key, or undefined if there was no mapping for key.
     */
    delete(key:K):V {
        let k = this._toString(key);
        let previousElement:IMapPair<K, V> = this.table[k];
        if (!CollectionTools.isUndefined(previousElement)) {
            delete this.table[k];
            this.p_length--;
            return previousElement.value;
        }
        return undefined;
    }


    /**
     * Returns an array containing all of the keys in this map.
     * @return {Array} an array containing all of the keys in this map.
     */
    get keys():K[] {
        let array:K[] = [];
        for (let name in this.table) {
            if ((<any>this.table).hasOwnProperty(name)) {
                let pair:IMapPair<K, V> = this.table[name];
                array.push(pair.key);
            }
        }
        return array;
    }


    /**
     * Returns an array containing all of the values in this map.
     * @return {Array} an array containing all of the values in this map.
     */
    get values():V[] {
        let array:V[] = [];
        for (let name in this.table) {
            if ((<any>this.table).hasOwnProperty(name)) {
                let pair:IMapPair<K, V> = this.table[name];
                array.push(pair.value);
            }
        }
        return array;
    }


    /**
     * Executes the provided function once for each key-value pair
     * present in this map.
     * @param {function(Object, Object):*} callback function to execute, it is
     * invoked with two arguments: value and key. To break the iteration you can
     * optionally return false.
     */
    forEach(callback:(value:V, key:K) => boolean):void {
        for (let name in this.table) {
            if ((<any>this.table).hasOwnProperty(name)) {
                let pair:IMapPair<K, V> = this.table[name];
                let ret = callback(pair.value, pair.key);
                if (ret === false) return;
            }
        }
    }


    /**
     * Returns true if this map has a mapping for the specified key.
     * @param {Object} key key whose presence in this map is to be tested.
     * @return {boolean} true if this map has a mapping for the specified key.
     */
    has(key:K):boolean { return !CollectionTools.isUndefined(this.get(key)); }


    /**
     * Removes all mappings from this map.
     * @this {CollectionTools.Map}
     */
    clear() {
        this.table = {};
        this.p_length = 0;
    }


    /**
     * Returns true if this map has no mappings.
     * @return {boolean} true if this map has no mappings.
     */
    get isEmpty():boolean { return this.p_length <= 0; }


    toString():string {
        let toret = "{";
        this.forEach((k, v):boolean => {
            toret = toret + "\n\t" + k.toString() + " : " + v.toString();
            return true;
        });
        return toret + "\n}";
    }
} // End class


export default MapList;
