package me.eater.threedom.dom

import me.eater.threedom.dom.event.*
import me.eater.threedom.dom.render.IRenderNode
import me.eater.threedom.dom.render.IRenderTarget
import me.eater.threedom.event.Event
import me.eater.threedom.event.EventListener
import me.eater.threedom.event.trigger
import me.eater.threedom.utils.ObservableSet
import me.eater.threedom.utils.joml.mutable
import me.eater.threedom.utils.joml.times
import org.joml.Matrix4d
import org.joml.Matrix4dc
import org.joml.Vector3dc

abstract class Node<T : INode<T>>(document: IDocument?) : INode<T>, EventListener {
    override val className: String = this::class.java.name

    override var document: IDocument? = document
        protected set
    override val nodeId = INode.getNextNodeId()

    private val nodes: MutableSet<INode<*>> = mutableSetOf()
    override var parentNode: INode<*>? = null
        protected set

    override var shouldIndexLocation: Boolean = false
        set(value) {
            if (field == value) return

            field = value
            trigger(NodeLocationIndexStateChange(field, this))
        }

    override var id: String? = null
        set(value) {
            val old = field
            field = value

            trigger(NodeIDUpdate(this, old, value))
        }

    override var absolute: Matrix4dc = Matrix4d()
        protected set

    override val tagList: MutableSet<String> = ObservableSet {
        when (it.action) {
            ObservableSet.Action.Removed -> trigger(NodeClassListUpdate.Removed(it.elements, this))
            ObservableSet.Action.Added -> trigger(NodeClassListUpdate.Added(it.elements, this))
        }
    }

    private inline fun <reified T> trigger(event: T) {
        if (parentNode !== null) {
            document?.trigger(Event(event, this))
        }
    }

    override var model: Matrix4dc = Matrix4d()
        set(value) {
            val old = Matrix4d(field)
            field = value.mutable()
            trigger(NodeModelUpdate(this, old))
        }

    fun model(block: Matrix4d.() -> Matrix4dc) {
        this.model = block(this.model.mutable())
    }

    var children: List<INode<*>> = nodes.toList()

    override fun addNode(newNode: INode<*>) {
        nodes.add(newNode)
        if (newNode.parentNode != this) {
            newNode.updateParent(this)
        }
    }

    override fun removeNode(refNode: INode<*>) {
        nodes.remove(refNode)
        if (refNode.parentNode == this) {
            refNode.updateParent(null)
        }
    }

    override fun removeAll() {
        nodes.forEach(::removeNode)
    }

    override fun replaceNode(newNode: INode<*>, refNode: INode<*>): Boolean {
        if (nodes.remove(refNode)) {
            nodes.add(newNode)
            return true
        }

        return false
    }

    override fun sequence(): Sequence<INode<*>> = nodes.asSequence()
    override fun iterator(): Iterator<INode<*>> = nodes.iterator()

    fun recursiveSequence(): Sequence<INode<*>> = sequence {
        val iterators = mutableListOf<Iterator<INode<*>>>()
        var current = iterator()
        while (true) {
            for (node in current) {
                yield(node)
                iterators.add(node.iterator())
            }

            current = iterators.firstOrNull() ?: return@sequence
        }
    }


    protected abstract fun cloneSelf(): T

    override fun clone(deep: Boolean): T {
        val cloned = cloneSelf()

        if (!deep) {
            return cloned
        }

        for (node in cloned) {
            cloned.addNode(node.clone(true))
        }

        return cloned
    }

    override fun updateParent(refNode: INode<*>?): Boolean {
        val parent = parentNode
        if (parent == refNode) {
            return true
        }

        if (refNode != null && !refNode.hasChild(this)) {
            return false
        }

        parentNode = refNode
        parent?.removeNode(this)

        val event = when {
            parent == null && refNode != null ->
                DOMTreeUpdate.Insert(refNode, this).also(::trigger)
            refNode == null && parent != null -> {
                val data = DOMTreeUpdate.Remove(parent, this)

                // Trigger on removed as well as on the parent
                // Since you can't bubble from a detached node
                var ev = Event(data, this)
                document?.trigger(this, ev)
                if (ev.bubble) {
                    document?.trigger(parent, Event(data, this))
                }

                // Trigger on removed as well as on the parent
                // Since you can't bubble from a detached node
                ev = Event(data, this)
                document?.trigger<DOMTreeUpdate>(this, ev)
                if (ev.bubble) {
                    document?.trigger<DOMTreeUpdate>(parent, Event(data, this))
                }

                return true
            }


            refNode != null && parent != null ->
                DOMTreeUpdate.Move(parent, refNode, this).also(::trigger)
            else -> return true
        }

        trigger(event)

        return true
    }

    override fun hasChild(refNode: INode<*>) = nodes.contains(refNode)

    override fun <T> on(eventName: String, block: (Event<T>) -> Unit) {
        document?.addEventListener(eventName, this, block)
    }

    override fun detachFromDocument() {
        val doc = this.document ?: return
        this.document = null
        doc.removeNode(this)
    }

    fun finalize() {
        document?.removeNode(this)
    }

    override fun getNodeById(id: String): INode<*>? = document?.getNodeById(id)?.takeIf { it.hasParent(this) }
    override fun getNodesByTag(tagName: String): Sequence<INode<*>> = if (nodes.isEmpty())
        emptySequence()
    else
        document?.getNodesByTag(tagName)?.filter { it.hasParent(this) } ?: emptySequence()

    override fun <T : INode<T>> getNodesByClass(className: String): Sequence<T> =
        document?.getNodesByClass<T>(className)?.filter { it.hasParent(this) } ?: emptySequence()

    override fun <T : IRenderTarget<C>, C> getNodesByRenderTarget(targetType: String): Sequence<IRenderNode<*, T, C>> =
        document?.getNodesByRenderTarget<T, C>(targetType)?.filter { it.hasParent(this) } ?: emptySequence()

    override fun updateAbsolute() {
        this.absolute = (this.parentNode?.absolute ?: Matrix4d()) * model
    }

    override fun findInRegion(pointA: Vector3dc, pointB: Vector3dc): Sequence<INode<*>> =
        document?.findInRegion(pointA, pointB)?.filter { it.hasParent(this) } ?: emptySequence()


    override fun findInRange(origin: Vector3dc, range: Number): Sequence<INode<*>> =
        document?.findInRange(origin, range)?.filter { it.hasParent(this) } ?: emptySequence()
}
