package ru.dbotthepony.mc.otm.graph

import ru.dbotthepony.mc.otm.core.util.IConditionalTickable
import ru.dbotthepony.mc.otm.core.util.ITickable
import java.lang.ref.WeakReference
import java.util.*
import kotlin.collections.ArrayDeque
import kotlin.collections.ArrayList

open class GraphNodeList<N : GraphNode<N, G>, G : GraphNodeList<N, G>> : IConditionalTickable {
	private val queuedAdd = ArrayDeque<N>()
	private val queuedRemove = ArrayDeque<N>()
	private val nodesInternal = ArrayList<N>()
	private val conditional = ArrayList<IConditionalTickable>()
	private val always = ArrayList<ITickable>()
	private var isTicking = false
	private var shouldQueueChanges = false

	protected val nodes: List<N> = Collections.unmodifiableList(nodesInternal)

	val size get() = nodesInternal.size

	var isValid = true
		private set

	/**
	 * Allows storing arbitrary data by external code
	 */
	val userData = HashMap<UUID, Any>()

	protected open fun onNodeRemoved(node: N) {}
	protected open fun onNodeAdded(node: N) {}

	protected open fun innerTick(): Boolean {
		return false
	}

	fun beginTicking(node: N) {
		require(node in nodesInternal || node in queuedAdd) { "Node $node does not belong to $this" }
		if (node in queuedRemove) return

		if (node is IConditionalTickable) {
			conditional.add(node)
			beginTicking()
		} else if (node is ITickable) {
			always.add(node)
			beginTicking()
		} else {
			throw ClassCastException("$node does not implement ITickable nor IConditionalTickable")
		}
	}

	private fun addNow(node: N) {
		node.graph = this as G
		nodesInternal.add(node)

		if (node is IConditionalTickable) {
			conditional.add(node)
			beginTicking()
		} else if (node is ITickable) {
			always.add(node)
			beginTicking()
		}

		onNodeAdded(node)
	}

	private fun removeNow(node: N, removeFromList: Boolean = true) {
		if (removeFromList)
			nodesInternal.remove(node)

		if (node is IConditionalTickable)
			conditional.remove(node)
		else if (node is ITickable)
			always.remove(node)

		onNodeRemoved(node)
	}

	private fun processAddQueue() {
		while (queuedAdd.isNotEmpty()) {
			addNow(queuedAdd.removeFirst())
		}
	}

	private fun processRemoveQueue() {
		while (queuedRemove.isNotEmpty()) {
			removeNow(queuedRemove.removeFirst())
		}
	}

	private fun processQueues() {
		processRemoveQueue()
		processAddQueue()
	}

	final override fun tick(): Boolean {
		if (!isValid)
			return false

		processQueues()
		conditional.removeIf { !it.tick() }
		for (node in always) node.tick()

		return innerTick() || always.isNotEmpty() || conditional.isNotEmpty()
	}

	fun addNodeQueued(node: N): Boolean {
		check(isValid) { "$this is no longer valid" }

		if (node in nodesInternal || node in queuedAdd)
			return false

		queuedAdd.add(node)
		beginTicking()
		return true
	}

	fun addNode(node: N): Boolean {
		check(isValid) { "$this is no longer valid" }

		if (node in nodesInternal)
			return false

		if (shouldQueueChanges) {
			if (node in queuedAdd) return false
			queuedAdd.add(node)
			beginTicking()
		} else {
			queuedAdd.remove(node)
			addNow(node)
		}

		return true
	}

	fun removeNode(node: N): Boolean {
		check(isValid) { "$this is no longer valid" }

		if (node !in nodesInternal)
			return false

		if (shouldQueueChanges) {
			if (node in queuedRemove) return false
			queuedRemove.add(node)
			beginTicking()
		} else {
			queuedRemove.remove(node)
			removeNow(node)
		}

		return true
	}

	fun retain(nodes: Set<N>) {
		check(isValid) { "$this is no longer valid" }
		queuedAdd.retainAll(nodes)
		nodesInternal.removeIf {
			if (it !in nodes) {
				removeNow(it, false)
				true
			} else {
				false
			}
		}
	}

	fun merge(other: G): G {
		if (other === this)
			return this

		check(isValid) { "$this is no longer valid" }

		if (size >= other.size) {
			shouldQueueChanges = true
			other.processQueues()
			other.nodesInternal.forEach { if (it.isValid) addNow(it) }
			processQueues()
			shouldQueueChanges = false

			other.isValid = false
			return this as G
		} else {
			return other.merge(this as G)
		}
	}

	private fun beginTicking() {
		if (!isTicking) {
			isTicking = true
			queue.add(WeakReference(this))
		}
	}

	companion object {
		private val graphs = ArrayList<WeakReference<GraphNodeList<*, *>>>()
		private val queue = ArrayList<WeakReference<GraphNodeList<*, *>>>()

		internal fun tick() {
			if (queue.isNotEmpty()) {
				graphs.addAll(queue)
				queue.clear()
			}

			val iterator = graphs.iterator()

			for (value in iterator) {
				val graph = value.get()

				if (graph == null || !graph.tick()) {
					graph?.isTicking = false
					iterator.remove()
				}
			}
		}
	}
}
