Events.ts

import {binaryInsert, stamp} from "./utils/shared";

export type EventCallback = (((...args: any[]) => boolean) | ((...args: any[]) => void));

export type EventCallbackStamp = EventCallback & { _openglobus_id?: number; _openglobus_priority?: number };

type EventCallbacks = Array<EventCallback>;
type EventCallbackHandler = { active: boolean; handlers: EventCallbacks };

export type EventsMap<T extends string[]> = {
    [K in T[number]]?: EventCallbackHandler
}

export type EventsHandler<T extends string[]> = Events<T> & EventsMap<T>;

export function createEvents<T extends string[]>(methodNames: T, sender?: any) {
    return new Events(methodNames, sender) as EventsHandler<T>;
}

/**
 * Base events class to handle custom events.
 * @class
 * @param {Array.<string>} [eventNames] - Event names that could be dispatched.
 * @param {*} [sender]
 */
export class Events<T extends string[]> {

    static __counter__: number = 0;

    protected __id: number;

    /**
     * Registered event names.
     * @protected
     * @type {T}
     */
    protected _eventNames: T;

    protected _sender: any;

    /**
     * Stop propagation flag
     * @protected
     * @type {boolean}
     */
    protected _stopPropagation: boolean;
    protected _stampCache: any;

    constructor(eventNames: T, sender?: any) {

        this.__id = Events.__counter__++;

        this._eventNames = [] as any;

        eventNames && this.registerNames(eventNames);

        this._sender = sender || this;

        this._stopPropagation = false;

        this._stampCache = {};
    }

    public bindSender(sender?: any) {
        this._sender = sender || this;
    }

    /**
     * Function that creates event object properties that would be dispatched.
     * @public
     * @param {Array.<string>} eventNames - Specified event names list.
     */
    public registerNames(eventNames: T): this {
        for (let i = 0; i < eventNames.length; i++) {
            (this as any)[eventNames[i]] = {
                active: true,
                handlers: []
            };
            this._eventNames.push(eventNames[i]);
        }
        return this;
    }

    protected _getStamp(name: string, id: number, ogid: number) {
        return `${name}_${id}_${ogid}`;
    }

    /**
     * Returns true if event callback has stamped.
     * @protected
     * @param {Object} name - Event identifier.
     * @param {Object} obj - Event callback.
     * @return {boolean} -
     */
    protected _stamp(name: string, obj: any) {
        let ogid = stamp(obj);
        let st = this._getStamp(name, this.__id, ogid);

        if (!this._stampCache[st]) {
            this._stampCache[st] = ogid;
            return true;
        }

        return false;
    }

    /**
     * Attach listener.
     * @public
     * @param {string} name - Event name to listen.
     * @param {EventCallback} callback - Event callback function.
     * @param {any} [sender] - Event callback function owner.
     * @param {number} [priority] - Priority of event callback.
     */
    public on(name: string, callback: EventCallback, sender?: any, priority: number = 0) {
        if (this._stamp(name, callback)) {
            if ((this as any)[name]) {
                let c = callback.bind(sender || this._sender) as EventCallbackStamp;
                c._openglobus_id = (callback as EventCallbackStamp)._openglobus_id;
                c._openglobus_priority = priority;
                binaryInsert((this as any)[name].handlers, c, (a: EventCallbackStamp, b: EventCallbackStamp) => {
                    return (b._openglobus_priority || 0) - (a._openglobus_priority || 0);
                });
            }
        }
    }

    /**
     * Stop listening event name with specified callback function.
     * @public
     * @param {string} name - Event name.
     * @param {EventCallback | null} callback - Attached  event callback.
     */
    public off(name: string, callback?: EventCallback | null) {
        if (callback) {
            let st = this._getStamp(name, this.__id, (callback as EventCallbackStamp)._openglobus_id!);
            if ((callback as EventCallbackStamp)._openglobus_id && this._stampCache[st]) {
                let h = (this as any)[name].handlers;
                let i = h.length;
                let indexToRemove = -1;
                while (i--) {
                    let hi = h[i];
                    if (hi._openglobus_id === (callback as EventCallbackStamp)._openglobus_id) {
                        indexToRemove = i;
                        break;
                    }
                }

                if (indexToRemove !== -1) {
                    h.splice(indexToRemove, 1);
                    this._stampCache[st] = undefined;
                    delete this._stampCache[st];
                }
            }
        }
    }

    /**
     * Dispatch event.
     * @public
     * @param {EventCallbackHandler} event - Event instance property that created by event name.
     * @param {Object} [args] - Callback parameters.
     */
    public dispatch(event: EventCallbackHandler | undefined, ...args: any[]) {
        let result = true;
        if (event && event.active && !this._stopPropagation) {
            let h = event.handlers.slice(0),
                i = h.length;
            while (i--) {
                if ((h[i] as any)(...args) === false) {
                    result = false;
                }
            }
        }
        this._stopPropagation = false;
        return result;
    }

    /**
     * Brakes events propagation.
     * @public
     */
    public stopPropagation() {
        this._stopPropagation = true;
    }

    /**
     * Removes all events.
     * @public
     */
    public clear() {
        for (let i = 0; i < this._eventNames.length; i++) {
            let e = (this as any)[this._eventNames[i]];
            e.handlers.length = 0;
            e.handlers = [];
        }
        this._eventNames.length = 0;
        this._eventNames = [] as any;
    }
}