package me.eater.threedom.dom

import me.eater.threedom.event.Event
import me.eater.threedom.generated.EventNames

class EventTree(block: EventTree.() -> Unit = {}) {
    private val topLevel: MutableMap<String, MutableSet<(Event<*>) -> Unit>> =
        mutableMapOf()
    private val listeners: MutableMap<String, MutableMap<Long, MutableSet<(Event<*>) -> Unit>>> =
        mutableMapOf()
    private val nodes: MutableMap<Long, MutableSet<String>> = mutableMapOf()

    init {
        block(this)
    }

    /**
     * Add an event listener for event [eventName] on top level, top level event listeners will be executed before all other event listeners
     */
    fun addTopLevelEventListener(eventName: String, handler: (Event<*>) -> Unit) {
        this.topLevel.getOrPut(eventName, ::mutableSetOf).add(handler)
    }

    /**
     * Add an event listener for event [eventName] on [target]
     */
    fun addEventListener(eventName: String, target: INode<*>, handler: (Event<*>) -> Unit) {
        this.listeners.getOrPut(eventName, ::mutableMapOf).getOrPut(target.nodeId, ::mutableSetOf).add(handler)
        this.nodes.getOrPut(target.nodeId, ::mutableSetOf).add(eventName)
    }

    /**
     * Remove node and all it's event listeners from event tree
     */
    fun removeNode(node: INode<*>) {
        val events = this.nodes.remove(node.nodeId) ?: return
        for (event in events) {
            this.listeners[event]?.remove(node.nodeId)
        }
    }

    /**
     * Trigger event [eventName] on [target]
     */
    fun trigger(eventName: String, target: INode<*>, event: Event<*>) {
        this.topLevel[eventName]?.let {
            for (handler in it) {
                handler(event)

                if (!event.propagate) {
                    return
                }
            }

            if (!event.bubble) {
                return
            }
        }

        val map = this.listeners[eventName] ?: return
        var current: INode<*>? = target

        while (current != null) {
            val set = map[current.nodeId]
            if (set != null) {
                for (eventHandler in set) {
                    eventHandler(event)

                    if (!event.propagate) {
                        break
                    }
                }
            }

            if (!event.bubble) {
                break
            }

            current = current.parentNode
        }
    }


    @Suppress("UNCHECKED_CAST")
    inline fun <reified T> on(target: INode<*>, noinline block: (Event<T>) -> Unit) =
        addEventListener(EventNames.getEventName<T>(), target, block as (Event<*>) -> Unit)

    @Suppress("UNCHECKED_CAST")
    inline fun <reified T> on(noinline block: (Event<T>) -> Unit) =
        addTopLevelEventListener(EventNames.getEventName<T>(), block as (Event<*>) -> Unit)

}
