sourceafQuickFlux::EventHub.fan

using afConcurrent

const class EventHub {
    private const LocalMap  stash   := LocalMap(typeof.qname)

    private Str:EventDef eventDefs {
        get { stash["eventDefs"] }
        set { stash["eventDefs"] = it }
    }

    private Str:EventListenerDef[] eventListeners {
        get { stash["eventListeners"] }
        set { stash["eventListeners"] = it }
    }

    private Int idSeq {
        get { stash["idSeq"] }
        set { stash["idSeq"] = it }
    }

    new make(EventDef[] eventDefs) {
        this.eventListeners = Str:EventListenerDef[][:] { caseInsensitive = true; def = [,].toImmutable }
        this.eventDefs      = Str:EventDef[:]           { caseInsensitive = true }
        this.idSeq          = 1
        
        eventDefs.each |eventDef| {   
            this.eventDefs[eventDef.name] = eventDef
        }
    }

    Str addListener(Enum event, Func eventListener) {
        addListener_(event.name, eventListener, false)
    }
    
    Void removeListener(Enum event, Str eventId) {
        eventName   := event.toStr
        eventDef    := eventDefs[eventName] ?: throw Err("Event '$eventName' has not been registered")
            
        listener := eventListeners[eventName].find {
            it.id == eventId
        } ?: throw Err("Event Id '$eventId' not found for Event '$eventName'")
        
        eventListeners[eventName].removeSame(listener)
    }

    virtual Void addEventSink(Obj eventSink) {
        handlerNames := eventDefs.map |eventDef->Str?| {
            eventName   := eventDef.name
            methodName  := "on$eventName.capitalize"

            if (eventSink.typeof.method(methodName, false) == null)
                return null
            
            method      := ReflectUtils.findMethod(eventSink.typeof, methodName, eventDef.signature, false)
            if (method == null) {
                method = eventSink.typeof.method(methodName)
                eventSig    := "${methodName}(" + eventDef.signature.join(", ") + ")"
                methodSig   := "${eventSink.typeof.qname}.${methodName}(" + method.params.join(", ") + ")"
                throw Err("Event handler ${methodSig} does not fit ${eventSig}")
            }
            listener    := |Obj[]? params| { method.callList([eventSink].addAll(params)) }
            addListener_(eventName, listener, true)
            return methodName
        }.exclude { it == null }.vals

        nonHandled := eventSink.typeof.methods
            .findAll { it.name.startsWith("on") }
            .exclude { handlerNames.contains(it.name) }
        if (!nonHandled.isEmpty)
            throw Err("Event sink $eventSink.typeof.qname defines unused methods : " + nonHandled.map { it.name }.join(", "))
    }

    Void fireEvent(Enum event,
        Obj? a := null, Obj? b := null, Obj? c := null, Obj? d := null,
        Obj? e := null, Obj? f := null, Obj? g := null, Obj? h := null) {
        params := ReflectUtils.toParamList(a, b, c, d, e, f, g, h)
            
        eventName   := event.toStr
        eventDef    := eventDefs[eventName] ?: throw Err("Event '$eventName' has not been registered")
            
        // for selectTank(null) else sys::ArgErr: Too few arguments: 1 < instance+1..1
        while (params.size < eventDef.signature.size)
            params.add(null)

        eventListeners[eventName].each {
            it.call(params)
        }
    }
    
    // ---- Private -------------------------------------------------------------------------------

    Str addListener_(Str eventName, Func eventListener, Bool paramsAsList) {
        eventDef    := eventDefs[eventName] ?: throw Err("Event '$eventName' has not been registered")
        id          := "${typeof.name}-${idSeq}"
        idSeq        = idSeq + 1
        eventListeners.getOrAdd(eventName) { EventListenerDef[,] } .add(EventListenerDef(eventDef, id, eventListener, paramsAsList))
        return id
    }
}

internal class EventListenerDef {
    EventDef    eventDef
    Func        eventListener
    Bool        paramsAsList
    Str         id

    new make(EventDef eventDef, Str id, |Obj?| eventListener, Bool paramsAsList) {
        this.eventDef       = eventDef
        this.id             = id
        this.eventListener  = eventListener
        this.paramsAsList   = paramsAsList
    }

    Obj call(Obj?[] params) {
        types := params.map { it?.typeof ?: Void#}
        if (!paramsAsList && !ReflectUtils.paramTypesFitFuncSignature(types, eventListener))
            throw Err("Event $eventDef.name Params do not fit definition : $types -> $eventListener.params")
        try {
            return eventListener.callList(paramsAsList ? [params] : params)
        } catch (Err e) {
            throw Err("Err calling $eventDef.name($eventDef.signature)", e)
        }
    }
}

const mixin EventDef {
    abstract Str name()
    abstract Type[] signature()
}

const class EventDefImpl : EventDef {
    override const Str name
    override const Type[] signature
    new make(Str module, Str name, Type[] signature := Type#.emptyList) {
        this.name       = name
        this.signature  = signature
    }
}