"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const utils_1 = require("../utils");
const lodash_isnil_1 = tslib_1.__importDefault(require("lodash.isnil"));
const deepmerge_1 = tslib_1.__importDefault(require("deepmerge"));
/**
 * A Base Registry
 */
class Registry {
    constructor(BB) {
        this.BB = BB;
        /** Internal data */
        this.data = new Map();
        /** List of subscriptions */
        this.subscriptions = new Map();
        //
    }
    /**
     * The get() method returns a specified registry item.
     * @param key
     */
    get(...keys) {
        let item;
        for (const key of keys) {
            item = this.data.get(key);
            if (!lodash_isnil_1.default(item)) {
                break;
            }
        }
        return item ? item : undefined;
    }
    /**
     * The set() method adds or updates an element with a specified
     * key and item to the registry.
     * @param key
     * @param value
     */
    set(key, item) {
        if (!this.isItem(item)) {
            throw Error('Could not set registry item. Reason: Unknown item type.');
        }
        const existingItem = this.get(key);
        if (existingItem) {
            item = deepmerge_1.default(existingItem, item);
        }
        // Override existing or create an new one
        const finalItem = this.createItem(key, item);
        this.data.set(key, finalItem);
        this.publish(key, finalItem);
        return this;
    }
    /**
     * The getValue() method returns a specified value from a Registry object.
     * @param key
     */
    getValue(...keys) {
        let item;
        for (const key of keys) {
            item = this.get(key);
            if (!lodash_isnil_1.default(item)) {
                break;
            }
        }
        return item ? item.value : undefined;
    }
    /**
     * The setValue() method adds or updates the value a registry item with the specified key.
     * @param key
     * @param value
     */
    setValue(key, value) {
        const item = this.get(key);
        // Override existing
        if (item) {
            item.value = value;
            return this.set(key, item);
        }
        // Create a new item
        return this.set(key, { value });
    }
    async register(key, item) {
        const args = this.getKeyAnyItem(key, item);
        if (this.isInputItem(args.item)) {
            this.set(args.key, args.item);
            return args.key;
        }
        else if (this.isInputValue(args.item)) {
            this.setValue(args.key, args.item);
            return args.key;
        }
        throw Error('Could not register item. Reason: Unknown item type.');
    }
    /**
     * Register a collection of items.
     * @param collection
     */
    async registerCollection(collection = []) {
        // If its an array
        if (Array.isArray(collection)) {
            for (const item of collection) {
                await this.register(item);
            }
            return;
        }
        // If its an object
        else if (collection === Object(collection)) {
            for (const key of Object.keys(collection)) {
                await this.register(key, collection[key]);
            }
            return;
        }
        throw Error('Could not register collection. Reason: Unknown collection type.');
    }
    /**
     * The has() method returns a boolean indicating whether an element
     * with the specified key exists or not.
     * @param key
     */
    has(key) {
        return this.data.has(key);
    }
    /**
     * The delete() method removes the specified element from a Registry object.
     * @param key
     */
    delete(key) {
        return this.data.delete(key);
    }
    /**
     * The clear() method removes all elements from a Registry object.
     */
    clear() {
        this.data.clear();
    }
    /**
     * The entries() method returns a new Iterator object that
     * contains the [key, value] pairs for each element in the Registry
     * object in insertion order.
     */
    entries() {
        return this.data.entries();
    }
    /**
     * The keys() method returns a new Iterator object that contains
     * the keys for each element in the Registry object in insertion order.
     */
    keys() {
        return this.data.keys();
    }
    /**
     * The values() method returns a new Iterator object that contains
     * the values for each element in the Registry object in insertion order.
     */
    values() {
        return this.data.values();
    }
    /**
     * Returns the number of items in the Registry.
     */
    size() {
        return this.data.size;
    }
    /**
     * The forEach() method executes a provided function once per
     * each key/value pair in the Registry object, in insertion order.
     * @param callbackfn
     * @param thisArg
     */
    forEach(callbackfn, thisArg) {
        this.data.forEach(callbackfn, thisArg);
    }
    /**
     * Filter registry items by a predicate function.
     * @param predicate
     */
    filter(predicate) {
        const arr = Array.from(this.entries()).filter(entry => predicate(entry[1].value, entry[0], entry[1]));
        const items = {};
        Array.from(arr).forEach(entry => {
            items[entry[0]] = entry[1];
        });
        return items;
    }
    /**
     * Filter registry items by a predicate function.
     * @param predicate
     */
    filterValues(predicate) {
        const arr = Array.from(this.entries()).filter(entry => predicate(entry[1].value, entry[0], entry[1]));
        const items = {};
        Array.from(arr).forEach(entry => {
            items[entry[0]] = entry[1].value;
        });
        return items;
    }
    /**
     * Subscribe to a config value update
     * @param key Config key
     * @param callback Callback function
     * @returns Subscription ID
     */
    subscribe(key, callback) {
        let subscriptions = this.subscriptions.get(key);
        // If theres no subscriptions map, create one
        if (!subscriptions) {
            subscriptions = new Map();
        }
        // Create a unique subscription ID
        const subId = utils_1.makeId();
        // Set the callback function
        subscriptions.set(subId, callback);
        // Save the updated item
        this.subscriptions.set(key, subscriptions);
        return subId;
    }
    /**
     * Unsubscribe from a config value update
     * @param key Config key
     * @param subscriptionId Subscription ID
     */
    unsubscribe(key, subscriptionId) {
        const subscriptions = this.subscriptions.get(key);
        if (!subscriptions) {
            // tslint:disable-next-line
            throw Error(`Could not unsubscribe from a registry item. Reason: No subsciptions for item with key \"${key}\" registered.`);
        }
        if (!subscriptions.get(subscriptionId)) {
            // tslint:disable-next-line
            throw Error(`Could not unsubscribe from a registry item. Reason: No subscription with id "${subscriptionId}" registered.`);
        }
        subscriptions.delete(subscriptionId);
        this.subscriptions.set(key, subscriptions);
    }
    /**
     * Convert any input value to an item. This is where you transform inputs and add defualts
     * @param key
     * @param partial
     */
    createItem(key, partial) {
        const item = Object.assign({}, partial, { key });
        if (!this.isItem(item)) {
            throw Error(`Could not set item. Reason: Unknown item type.`);
        }
        return item;
    }
    /**
     * Typeguard to check a given object is a registry value
     * @param value
     */
    isValue(value) {
        return value !== undefined;
    }
    /**
     * Typeguard to check a given object is an input value
     * @param value
     */
    isInputValue(value) {
        return value !== undefined;
    }
    /**
     * Typeguard to check a given object is a registry item
     * @param item
     */
    isItem(item) {
        return item.value !== undefined;
    }
    /**
     * Typeguard to check a given object is a input item
     * @param item
     */
    isInputItem(item) {
        return item.value !== undefined;
    }
    /**
     * Publishes updates of a config value change to all the subscribers
     * @param key
     * @param item
     */
    publish(key, item) {
        const subscriptions = this.subscriptions.get(key);
        if (!subscriptions) {
            return;
        }
        subscriptions.forEach(fn => fn(item.value, item));
    }
    /**
     * Find one item in a given sequence. Returns the first item found.
     * @param keys
     */
    findOne(...keys) {
        for (const tempKey of keys) {
            const item = this.data.get(tempKey);
            if (!lodash_isnil_1.default(item)) {
                return item;
            }
        }
        return;
    }
    getKeyAnyItem(key, item) {
        let finalKey, finalItem;
        // 2 params were passed, key and item
        if (typeof key === 'string' && item !== undefined) {
            finalKey = key;
            finalItem = item;
        }
        // Only one param was passed to the function, this should be an item
        else if (key && (this.isItem(key) || this.isInputItem(key))) {
            finalKey = key.key;
            finalItem = key;
        }
        // No key exists, attempt to generate one
        if (!finalKey) {
            finalKey = this.generateKey(finalItem);
        }
        if (!finalKey) {
            throw Error('Could not register item. Reason: No key given.');
        }
        if (finalItem === undefined) {
            throw Error('Could not register item. Reason: No item given.');
        }
        return { key: finalKey, item: utils_1.getDefiniteModule(finalItem) };
    }
    /**
     * This function is used to auto generate an item key
     * @param item
     */
    generateKey(_item) {
        return utils_1.makeId();
    }
}
exports.Registry = Registry;
