import { find, each, ArrayExt } from '@lumino/algorithm';
import { AttachedProperty } from '@lumino/properties';

// Copyright (c) Jupyter Development Team.
/**
 * A concrete implementation of `ISignal`.
 *
 * #### Example
 * ```typescript
 * import { ISignal, Signal } from '@lumino/signaling';
 *
 * class SomeClass {
 *
 *   constructor(name: string) {
 *     this.name = name;
 *   }
 *
 *   readonly name: string;
 *
 *   get valueChanged: ISignal<this, number> {
 *     return this._valueChanged;
 *   }
 *
 *   get value(): number {
 *     return this._value;
 *   }
 *
 *   set value(value: number) {
 *     if (value === this._value) {
 *       return;
 *     }
 *     this._value = value;
 *     this._valueChanged.emit(value);
 *   }
 *
 *   private _value = 0;
 *   private _valueChanged = new Signal<this, number>(this);
 * }
 *
 * function logger(sender: SomeClass, value: number): void {
 *   console.log(sender.name, value);
 * }
 *
 * let m1 = new SomeClass('foo');
 * let m2 = new SomeClass('bar');
 *
 * m1.valueChanged.connect(logger);
 * m2.valueChanged.connect(logger);
 *
 * m1.value = 42;  // logs: foo 42
 * m2.value = 17;  // logs: bar 17
 * ```
 */
var Signal = /** @class */ (function () {
    /**
     * Construct a new signal.
     *
     * @param sender - The sender which owns the signal.
     */
    function Signal(sender) {
        this._blockedCount = 0;
        this.sender = sender;
    }
    /**
     * Block the signal during the execution of a callback.
     *
     * ### Notes
     * The callback function must be synchronous.
     *
     * @deprecated This feature will be removed in Lumino 2.
     *
     * @param fn The callback during which the signal is blocked
     */
    Signal.prototype.block = function (fn) {
        this._blockedCount++;
        try {
            fn();
        }
        finally {
            this._blockedCount--;
        }
    };
    /**
     * Connect a slot to the signal.
     *
     * @param slot - The slot to invoke when the signal is emitted.
     *
     * @param thisArg - The `this` context for the slot. If provided,
     *   this must be a non-primitive object.
     *
     * @returns `true` if the connection succeeds, `false` otherwise.
     */
    Signal.prototype.connect = function (slot, thisArg) {
        return Private.connect(this, slot, thisArg);
    };
    /**
     * Disconnect a slot from the signal.
     *
     * @param slot - The slot to disconnect from the signal.
     *
     * @param thisArg - The `this` context for the slot. If provided,
     *   this must be a non-primitive object.
     *
     * @returns `true` if the connection is removed, `false` otherwise.
     */
    Signal.prototype.disconnect = function (slot, thisArg) {
        return Private.disconnect(this, slot, thisArg);
    };
    /**
     * Emit the signal and invoke the connected slots.
     *
     * @param args - The args to pass to the connected slots.
     *
     * #### Notes
     * Slots are invoked synchronously in connection order.
     *
     * Exceptions thrown by connected slots will be caught and logged.
     */
    Signal.prototype.emit = function (args) {
        if (!this._blockedCount) {
            Private.emit(this, args);
        }
    };
    return Signal;
}());
/**
 * The namespace for the `Signal` class statics.
 */
(function (Signal) {
    /**
     * Block all signals emitted by an object during
     * the execution of a callback.
     *
     * ### Notes
     * The callback function must be synchronous.
     *
     * @deprecated This feature will be removed in Lumino 2.
     *
     * @param sender The signals sender
     * @param fn The callback during which all signals are blocked
     */
    function blockAll(sender, fn) {
        var blockedProperty = Private.blockedProperty;
        blockedProperty.set(sender, blockedProperty.get(sender) + 1);
        try {
            fn();
        }
        finally {
            blockedProperty.set(sender, blockedProperty.get(sender) - 1);
        }
    }
    Signal.blockAll = blockAll;
    /**
     * Remove all connections between a sender and receiver.
     *
     * @param sender - The sender object of interest.
     *
     * @param receiver - The receiver object of interest.
     *
     * #### Notes
     * If a `thisArg` is provided when connecting a signal, that object
     * is considered the receiver. Otherwise, the `slot` is considered
     * the receiver.
     */
    function disconnectBetween(sender, receiver) {
        Private.disconnectBetween(sender, receiver);
    }
    Signal.disconnectBetween = disconnectBetween;
    /**
     * Remove all connections where the given object is the sender.
     *
     * @param sender - The sender object of interest.
     */
    function disconnectSender(sender) {
        Private.disconnectSender(sender);
    }
    Signal.disconnectSender = disconnectSender;
    /**
     * Remove all connections where the given object is the receiver.
     *
     * @param receiver - The receiver object of interest.
     *
     * #### Notes
     * If a `thisArg` is provided when connecting a signal, that object
     * is considered the receiver. Otherwise, the `slot` is considered
     * the receiver.
     */
    function disconnectReceiver(receiver) {
        Private.disconnectReceiver(receiver);
    }
    Signal.disconnectReceiver = disconnectReceiver;
    /**
     * Remove all connections where an object is the sender or receiver.
     *
     * @param object - The object of interest.
     *
     * #### Notes
     * If a `thisArg` is provided when connecting a signal, that object
     * is considered the receiver. Otherwise, the `slot` is considered
     * the receiver.
     */
    function disconnectAll(object) {
        Private.disconnectAll(object);
    }
    Signal.disconnectAll = disconnectAll;
    /**
     * Clear all signal data associated with the given object.
     *
     * @param object - The object for which the data should be cleared.
     *
     * #### Notes
     * This removes all signal connections and any other signal data
     * associated with the object.
     */
    function clearData(object) {
        Private.disconnectAll(object);
    }
    Signal.clearData = clearData;
    /**
     * Get the signal exception handler.
     *
     * @returns The current exception handler.
     *
     * #### Notes
     * The default exception handler is `console.error`.
     */
    function getExceptionHandler() {
        return Private.exceptionHandler;
    }
    Signal.getExceptionHandler = getExceptionHandler;
    /**
     * Set the signal exception handler.
     *
     * @param handler - The function to use as the exception handler.
     *
     * @returns The old exception handler.
     *
     * #### Notes
     * The exception handler is invoked when a slot throws an exception.
     */
    function setExceptionHandler(handler) {
        var old = Private.exceptionHandler;
        Private.exceptionHandler = handler;
        return old;
    }
    Signal.setExceptionHandler = setExceptionHandler;
})(Signal || (Signal = {}));
/**
 * The namespace for the module implementation details.
 */
var Private;
(function (Private) {
    /**
     * The signal exception handler function.
     */
    Private.exceptionHandler = function (err) {
        console.error(err);
    };
    /**
     * Connect a slot to a signal.
     *
     * @param signal - The signal of interest.
     *
     * @param slot - The slot to invoke when the signal is emitted.
     *
     * @param thisArg - The `this` context for the slot. If provided,
     *   this must be a non-primitive object.
     *
     * @returns `true` if the connection succeeds, `false` otherwise.
     */
    function connect(signal, slot, thisArg) {
        // Coerce a `null` `thisArg` to `undefined`.
        thisArg = thisArg || undefined;
        // Ensure the sender's array of receivers is created.
        var receivers = receiversForSender.get(signal.sender);
        if (!receivers) {
            receivers = [];
            receiversForSender.set(signal.sender, receivers);
        }
        // Bail if a matching connection already exists.
        if (findConnection(receivers, signal, slot, thisArg)) {
            return false;
        }
        // Choose the best object for the receiver.
        var receiver = thisArg || slot;
        // Ensure the receiver's array of senders is created.
        var senders = sendersForReceiver.get(receiver);
        if (!senders) {
            senders = [];
            sendersForReceiver.set(receiver, senders);
        }
        // Create a new connection and add it to the end of each array.
        var connection = { signal: signal, slot: slot, thisArg: thisArg };
        receivers.push(connection);
        senders.push(connection);
        // Indicate a successful connection.
        return true;
    }
    Private.connect = connect;
    /**
     * Disconnect a slot from a signal.
     *
     * @param signal - The signal of interest.
     *
     * @param slot - The slot to disconnect from the signal.
     *
     * @param thisArg - The `this` context for the slot. If provided,
     *   this must be a non-primitive object.
     *
     * @returns `true` if the connection is removed, `false` otherwise.
     */
    function disconnect(signal, slot, thisArg) {
        // Coerce a `null` `thisArg` to `undefined`.
        thisArg = thisArg || undefined;
        // Lookup the list of receivers, and bail if none exist.
        var receivers = receiversForSender.get(signal.sender);
        if (!receivers || receivers.length === 0) {
            return false;
        }
        // Bail if no matching connection exits.
        var connection = findConnection(receivers, signal, slot, thisArg);
        if (!connection) {
            return false;
        }
        // Choose the best object for the receiver.
        var receiver = thisArg || slot;
        // Lookup the array of senders, which is now known to exist.
        var senders = sendersForReceiver.get(receiver);
        // Clear the connection and schedule cleanup of the arrays.
        connection.signal = null;
        scheduleCleanup(receivers);
        scheduleCleanup(senders);
        // Indicate a successful disconnection.
        return true;
    }
    Private.disconnect = disconnect;
    /**
     * Remove all connections between a sender and receiver.
     *
     * @param sender - The sender object of interest.
     *
     * @param receiver - The receiver object of interest.
     */
    function disconnectBetween(sender, receiver) {
        // If there are no receivers, there is nothing to do.
        var receivers = receiversForSender.get(sender);
        if (!receivers || receivers.length === 0) {
            return;
        }
        // If there are no senders, there is nothing to do.
        var senders = sendersForReceiver.get(receiver);
        if (!senders || senders.length === 0) {
            return;
        }
        // Clear each connection between the sender and receiver.
        each(senders, function (connection) {
            // Skip connections which have already been cleared.
            if (!connection.signal) {
                return;
            }
            // Clear the connection if it matches the sender.
            if (connection.signal.sender === sender) {
                connection.signal = null;
            }
        });
        // Schedule a cleanup of the senders and receivers.
        scheduleCleanup(receivers);
        scheduleCleanup(senders);
    }
    Private.disconnectBetween = disconnectBetween;
    /**
     * Remove all connections where the given object is the sender.
     *
     * @param sender - The sender object of interest.
     */
    function disconnectSender(sender) {
        // If there are no receivers, there is nothing to do.
        var receivers = receiversForSender.get(sender);
        if (!receivers || receivers.length === 0) {
            return;
        }
        // Clear each receiver connection.
        each(receivers, function (connection) {
            // Skip connections which have already been cleared.
            if (!connection.signal) {
                return;
            }
            // Choose the best object for the receiver.
            var receiver = connection.thisArg || connection.slot;
            // Clear the connection.
            connection.signal = null;
            // Cleanup the array of senders, which is now known to exist.
            scheduleCleanup(sendersForReceiver.get(receiver));
        });
        // Schedule a cleanup of the receivers.
        scheduleCleanup(receivers);
    }
    Private.disconnectSender = disconnectSender;
    /**
     * Remove all connections where the given object is the receiver.
     *
     * @param receiver - The receiver object of interest.
     */
    function disconnectReceiver(receiver) {
        // If there are no senders, there is nothing to do.
        var senders = sendersForReceiver.get(receiver);
        if (!senders || senders.length === 0) {
            return;
        }
        // Clear each sender connection.
        each(senders, function (connection) {
            // Skip connections which have already been cleared.
            if (!connection.signal) {
                return;
            }
            // Lookup the sender for the connection.
            var sender = connection.signal.sender;
            // Clear the connection.
            connection.signal = null;
            // Cleanup the array of receivers, which is now known to exist.
            scheduleCleanup(receiversForSender.get(sender));
        });
        // Schedule a cleanup of the list of senders.
        scheduleCleanup(senders);
    }
    Private.disconnectReceiver = disconnectReceiver;
    /**
     * Remove all connections where an object is the sender or receiver.
     *
     * @param object - The object of interest.
     */
    function disconnectAll(object) {
        // Remove all connections where the given object is the sender.
        disconnectSender(object);
        // Remove all connections where the given object is the receiver.
        disconnectReceiver(object);
    }
    Private.disconnectAll = disconnectAll;
    /**
     * Emit a signal and invoke its connected slots.
     *
     * @param signal - The signal of interest.
     *
     * @param args - The args to pass to the connected slots.
     *
     * #### Notes
     * Slots are invoked synchronously in connection order.
     *
     * Exceptions thrown by connected slots will be caught and logged.
     */
    function emit(signal, args) {
        if (Private.blockedProperty.get(signal.sender) > 0) {
            return;
        }
        // If there are no receivers, there is nothing to do.
        var receivers = receiversForSender.get(signal.sender);
        if (!receivers || receivers.length === 0) {
            return;
        }
        // Invoke the slots for connections with a matching signal.
        // Any connections added during emission are not invoked.
        for (var i = 0, n = receivers.length; i < n; ++i) {
            var connection = receivers[i];
            if (connection.signal === signal) {
                invokeSlot(connection, args);
            }
        }
    }
    Private.emit = emit;
    /**
     * A weak mapping of sender to array of receiver connections.
     */
    var receiversForSender = new WeakMap();
    /**
     * A weak mapping of receiver to array of sender connections.
     */
    var sendersForReceiver = new WeakMap();
    /**
     * A set of connection arrays which are pending cleanup.
     */
    var dirtySet = new Set();
    /**
     * A function to schedule an event loop callback.
     */
    var schedule = (function () {
        var ok = typeof requestAnimationFrame === 'function';
        // @ts-ignore
        return ok ? requestAnimationFrame : setImmediate;
    })();
    /**
     * Find a connection which matches the given parameters.
     */
    function findConnection(connections, signal, slot, thisArg) {
        return find(connections, function (connection) {
            return connection.signal === signal &&
                connection.slot === slot &&
                connection.thisArg === thisArg;
        });
    }
    /**
     * Invoke a slot with the given parameters.
     *
     * The connection is assumed to be valid.
     *
     * Exceptions in the slot will be caught and logged.
     */
    function invokeSlot(connection, args) {
        var signal = connection.signal, slot = connection.slot, thisArg = connection.thisArg;
        try {
            slot.call(thisArg, signal.sender, args);
        }
        catch (err) {
            Private.exceptionHandler(err);
        }
    }
    /**
     * Schedule a cleanup of a connection array.
     *
     * This will add the array to the dirty set and schedule a deferred
     * cleanup of the array contents. On cleanup, any connection with a
     * `null` signal will be removed from the array.
     */
    function scheduleCleanup(array) {
        if (dirtySet.size === 0) {
            schedule(cleanupDirtySet);
        }
        dirtySet.add(array);
    }
    /**
     * Cleanup the connection lists in the dirty set.
     *
     * This function should only be invoked asynchronously, when the
     * stack frame is guaranteed to not be on the path of user code.
     */
    function cleanupDirtySet() {
        dirtySet.forEach(cleanupConnections);
        dirtySet.clear();
    }
    /**
     * Cleanup the dirty connections in a connections array.
     *
     * This will remove any connection with a `null` signal.
     *
     * This function should only be invoked asynchronously, when the
     * stack frame is guaranteed to not be on the path of user code.
     */
    function cleanupConnections(connections) {
        ArrayExt.removeAllWhere(connections, isDeadConnection);
    }
    /**
     * Test whether a connection is dead.
     *
     * A dead connection has a `null` signal.
     */
    function isDeadConnection(connection) {
        return connection.signal === null;
    }
    /**
     * A property indicating a sender has been blocked if its value is not 0.
     */
    Private.blockedProperty = new AttachedProperty({
        name: 'blocked',
        create: function () { return 0; }
    });
})(Private || (Private = {}));

export { Signal };
//# sourceMappingURL=index.es6.js.map
