import { Dictionary, WithThisType } from './types'; 
 | 
  
 | 
// Return true to cancel bubble 
 | 
export type EventCallbackSingleParam<EvtParam = any> = EvtParam extends any 
 | 
    ? (params: EvtParam) => boolean | void 
 | 
    : never 
 | 
  
 | 
export type EventCallback<EvtParams = any[]> = EvtParams extends any[] 
 | 
    ? (...args: EvtParams) => boolean | void 
 | 
    : never 
 | 
export type EventQuery = string | Object 
 | 
  
 | 
type CbThis<Ctx, Impl> = unknown extends Ctx ? Impl : Ctx; 
 | 
  
 | 
type EventHandler<Ctx, Impl, EvtParams> = { 
 | 
    h: EventCallback<EvtParams> 
 | 
    ctx: CbThis<Ctx, Impl> 
 | 
    query: EventQuery 
 | 
  
 | 
    callAtLast: boolean 
 | 
} 
 | 
  
 | 
type DefaultEventDefinition = Dictionary<EventCallback<any[]>>; 
 | 
  
 | 
export interface EventProcessor<EvtDef = DefaultEventDefinition> { 
 | 
    normalizeQuery?: (query: EventQuery) => EventQuery 
 | 
    filter?: (eventType: keyof EvtDef, query: EventQuery) => boolean 
 | 
    afterTrigger?: (eventType: keyof EvtDef) => void 
 | 
} 
 | 
  
 | 
/** 
 | 
 * Event dispatcher. 
 | 
 * 
 | 
 * Event can be defined in EvtDef to enable type check. For example: 
 | 
 * ```ts 
 | 
 * interface FooEvents { 
 | 
 *     // key: event name, value: the first event param in `trigger` and `callback`. 
 | 
 *     myevent: { 
 | 
 *        aa: string; 
 | 
 *        bb: number; 
 | 
 *     }; 
 | 
 * } 
 | 
 * class Foo extends Eventful<FooEvents> { 
 | 
 *     fn() { 
 | 
 *         // Type check of event name and the first event param is enabled here. 
 | 
 *         this.trigger('myevent', {aa: 'xx', bb: 3}); 
 | 
 *     } 
 | 
 * } 
 | 
 * let foo = new Foo(); 
 | 
 * // Type check of event name and the first event param is enabled here. 
 | 
 * foo.on('myevent', (eventParam) => { ... }); 
 | 
 * ``` 
 | 
 * 
 | 
 * @param eventProcessor The object eventProcessor is the scope when 
 | 
 *        `eventProcessor.xxx` called. 
 | 
 * @param eventProcessor.normalizeQuery 
 | 
 *        param: {string|Object} Raw query. 
 | 
 *        return: {string|Object} Normalized query. 
 | 
 * @param eventProcessor.filter Event will be dispatched only 
 | 
 *        if it returns `true`. 
 | 
 *        param: {string} eventType 
 | 
 *        param: {string|Object} query 
 | 
 *        return: {boolean} 
 | 
 * @param eventProcessor.afterTrigger Called after all handlers called. 
 | 
 *        param: {string} eventType 
 | 
 */ 
 | 
export default class Eventful<EvtDef extends DefaultEventDefinition = DefaultEventDefinition> { 
 | 
  
 | 
    private _$handlers: Dictionary<EventHandler<any, any, any[]>[]> 
 | 
  
 | 
    protected _$eventProcessor: EventProcessor<EvtDef> 
 | 
  
 | 
    constructor(eventProcessors?: EventProcessor<EvtDef>) { 
 | 
        if (eventProcessors) { 
 | 
            this._$eventProcessor = eventProcessors; 
 | 
        } 
 | 
    } 
 | 
  
 | 
    on<Ctx, EvtNm extends keyof EvtDef>( 
 | 
        event: EvtNm, 
 | 
        handler: WithThisType<EvtDef[EvtNm], CbThis<Ctx, this>>, 
 | 
        context?: Ctx 
 | 
    ): this 
 | 
    on<Ctx, EvtNm extends keyof EvtDef>( 
 | 
        event: EvtNm, 
 | 
        query: EventQuery, 
 | 
        handler: WithThisType<EvtDef[EvtNm], CbThis<Ctx, this>>, 
 | 
        context?: Ctx 
 | 
    ): this 
 | 
    /** 
 | 
     * Bind a handler. 
 | 
     * 
 | 
     * @param event The event name. 
 | 
     * @param Condition used on event filter. 
 | 
     * @param handler The event handler. 
 | 
     * @param context 
 | 
     */ 
 | 
    on<Ctx, EvtNm extends keyof EvtDef>( 
 | 
        event: EvtNm, 
 | 
        query: EventQuery | WithThisType<EventCallback<EvtDef[EvtNm]>, CbThis<Ctx, this>>, 
 | 
        handler?: WithThisType<EventCallback<EvtDef[EvtNm]>, CbThis<Ctx, this>> | Ctx, 
 | 
        context?: Ctx 
 | 
    ): this { 
 | 
        if (!this._$handlers) { 
 | 
            this._$handlers = {}; 
 | 
        } 
 | 
  
 | 
        const _h = this._$handlers; 
 | 
  
 | 
        if (typeof query === 'function') { 
 | 
            context = handler as Ctx; 
 | 
            handler = query as (...args: any) => any; 
 | 
            query = null; 
 | 
        } 
 | 
  
 | 
        if (!handler || !event) { 
 | 
            return this; 
 | 
        } 
 | 
  
 | 
        const eventProcessor = this._$eventProcessor; 
 | 
        if (query != null && eventProcessor && eventProcessor.normalizeQuery) { 
 | 
            query = eventProcessor.normalizeQuery(query); 
 | 
        } 
 | 
  
 | 
        if (!_h[event as string]) { 
 | 
            _h[event as string] = []; 
 | 
        } 
 | 
  
 | 
        for (let i = 0; i < _h[event as string].length; i++) { 
 | 
            if (_h[event as string][i].h === handler) { 
 | 
                return this; 
 | 
            } 
 | 
        } 
 | 
  
 | 
        const wrap: EventHandler<Ctx, this, unknown[]> = { 
 | 
            h: handler as EventCallback<unknown[]>, 
 | 
            query: query, 
 | 
            ctx: (context || this) as CbThis<Ctx, this>, 
 | 
            // FIXME 
 | 
            // Do not publish this feature util it is proved that it makes sense. 
 | 
            callAtLast: (handler as any).zrEventfulCallAtLast 
 | 
        }; 
 | 
  
 | 
        const lastIndex = _h[event as string].length - 1; 
 | 
        const lastWrap = _h[event as string][lastIndex]; 
 | 
        (lastWrap && lastWrap.callAtLast) 
 | 
            ? _h[event as string].splice(lastIndex, 0, wrap) 
 | 
            : _h[event as string].push(wrap); 
 | 
  
 | 
        return this; 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * Whether any handler has bound. 
 | 
     */ 
 | 
    isSilent(eventName: keyof EvtDef): boolean { 
 | 
        const _h = this._$handlers; 
 | 
        return !_h || !_h[eventName as string] || !_h[eventName as string].length; 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * Unbind a event. 
 | 
     * 
 | 
     * @param eventType The event name. 
 | 
     *        If no `event` input, "off" all listeners. 
 | 
     * @param handler The event handler. 
 | 
     *        If no `handler` input, "off" all listeners of the `event`. 
 | 
     */ 
 | 
    off(eventType?: keyof EvtDef, handler?: Function): this { 
 | 
        const _h = this._$handlers; 
 | 
  
 | 
        if (!_h) { 
 | 
            return this; 
 | 
        } 
 | 
  
 | 
        if (!eventType) { 
 | 
            this._$handlers = {}; 
 | 
            return this; 
 | 
        } 
 | 
  
 | 
        if (handler) { 
 | 
            if (_h[eventType as string]) { 
 | 
                const newList = []; 
 | 
                for (let i = 0, l = _h[eventType as string].length; i < l; i++) { 
 | 
                    if (_h[eventType as string][i].h !== handler) { 
 | 
                        newList.push(_h[eventType as string][i]); 
 | 
                    } 
 | 
                } 
 | 
                _h[eventType as string] = newList; 
 | 
            } 
 | 
  
 | 
            if (_h[eventType as string] && _h[eventType as string].length === 0) { 
 | 
                delete _h[eventType as string]; 
 | 
            } 
 | 
        } 
 | 
        else { 
 | 
            delete _h[eventType as string]; 
 | 
        } 
 | 
  
 | 
        return this; 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * Dispatch a event. 
 | 
     * 
 | 
     * @param {string} eventType The event name. 
 | 
     */ 
 | 
    trigger<EvtNm extends keyof EvtDef>( 
 | 
        eventType: EvtNm, 
 | 
        ...args: Parameters<EvtDef[EvtNm]> 
 | 
    ): this { 
 | 
        if (!this._$handlers) { 
 | 
            return this; 
 | 
        } 
 | 
  
 | 
        const _h = this._$handlers[eventType as string]; 
 | 
        const eventProcessor = this._$eventProcessor; 
 | 
  
 | 
        if (_h) { 
 | 
            const argLen = args.length; 
 | 
  
 | 
            const len = _h.length; 
 | 
            for (let i = 0; i < len; i++) { 
 | 
                const hItem = _h[i]; 
 | 
                if (eventProcessor 
 | 
                    && eventProcessor.filter 
 | 
                    && hItem.query != null 
 | 
                    && !eventProcessor.filter(eventType, hItem.query) 
 | 
                ) { 
 | 
                    continue; 
 | 
                } 
 | 
  
 | 
                // Optimize advise from backbone 
 | 
                switch (argLen) { 
 | 
                    case 0: 
 | 
                        hItem.h.call(hItem.ctx); 
 | 
                        break; 
 | 
                    case 1: 
 | 
                        hItem.h.call(hItem.ctx, args[0]); 
 | 
                        break; 
 | 
                    case 2: 
 | 
                        hItem.h.call(hItem.ctx, args[0], args[1]); 
 | 
                        break; 
 | 
                    default: 
 | 
                        // have more than 2 given arguments 
 | 
                        hItem.h.apply(hItem.ctx, args); 
 | 
                        break; 
 | 
                } 
 | 
            } 
 | 
        } 
 | 
  
 | 
        eventProcessor && eventProcessor.afterTrigger 
 | 
            && eventProcessor.afterTrigger(eventType); 
 | 
  
 | 
        return this; 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * Dispatch a event with context, which is specified at the last parameter. 
 | 
     * 
 | 
     * @param {string} type The event name. 
 | 
     */ 
 | 
    triggerWithContext(type: keyof EvtDef, ...args: any[]): this { 
 | 
        if (!this._$handlers) { 
 | 
            return this; 
 | 
        } 
 | 
  
 | 
        const _h = this._$handlers[type as string]; 
 | 
        const eventProcessor = this._$eventProcessor; 
 | 
  
 | 
        if (_h) { 
 | 
            const argLen = args.length; 
 | 
            const ctx = args[argLen - 1]; 
 | 
  
 | 
            const len = _h.length; 
 | 
            for (let i = 0; i < len; i++) { 
 | 
                const hItem = _h[i]; 
 | 
                if (eventProcessor 
 | 
                    && eventProcessor.filter 
 | 
                    && hItem.query != null 
 | 
                    && !eventProcessor.filter(type, hItem.query) 
 | 
                ) { 
 | 
                    continue; 
 | 
                } 
 | 
  
 | 
                // Optimize advise from backbone 
 | 
                switch (argLen) { 
 | 
                    case 0: 
 | 
                        hItem.h.call(ctx); 
 | 
                        break; 
 | 
                    case 1: 
 | 
                        hItem.h.call(ctx, args[0]); 
 | 
                        break; 
 | 
                    case 2: 
 | 
                        hItem.h.call(ctx, args[0], args[1]); 
 | 
                        break; 
 | 
                    default: 
 | 
                        // have more than 2 given arguments 
 | 
                        hItem.h.apply(ctx, args.slice(1, argLen - 1)); 
 | 
                        break; 
 | 
                } 
 | 
            } 
 | 
        } 
 | 
  
 | 
        eventProcessor && eventProcessor.afterTrigger 
 | 
            && eventProcessor.afterTrigger(type); 
 | 
  
 | 
        return this; 
 | 
    } 
 | 
  
 | 
} 
 |