package ru.dbotthepony.kstarbound.server.world

import it.unimi.dsi.fastutil.objects.ObjectArrayList
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import org.apache.logging.log4j.LogManager
import org.classdump.luna.ByteString
import ru.dbotthepony.kstarbound.lua.tableMapOf
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.world.entities.tile.WorldObject

/**
 * Wiring network processor from original engine
 *
 * This processor has several downsides, such as performance degrading as more
 * entities are present in world (because this wire processor considers all entities in world on
 * each tick), and it also goes for "all or nothing" strategy the hard way;
 *
 * meaning, it won't finish a tick until all wired entities are loaded/available
 *
 * This processor is kept in new engine to allow mods to get original behavior of wiring network,
 * should new behavior be undesirable.
 */
class LegacyWireProcessor(val world: ServerWorld) {
	private val entities = HashMap<Vector2i, WorldObject>(1024, 0.5f)
	private val entitySet = HashSet<WorldObject>(1024, 0.5f)
	private var unopenNodes = HashMap<Vector2i, ObjectArrayList<WorldObject>>(1024, 0.5f)

	private var isTicking = false

	// TODO: this keeps chunks loaded for every wire network indefinitely
	//  need to implement concept of hierarchical tickets to solve this (so entities in network do not prolong liveliness of chunks they reside in)
	private suspend fun tick0() {
		val tickets = ObjectArrayList<ServerChunk.ITicket>()

		try {
			world.entities.values.forEach {
				if (it is WorldObject) {
					populateWorking(it)

					val ticket = world.permanentChunkTicket(world.geometry.chunkFromCell(it.tilePosition)).await()

					if (ticket != null)
						tickets.add(ticket)
				}
			}

			while (unopenNodes.isNotEmpty()) {
				val copy = unopenNodes
				unopenNodes = HashMap(1024, 0.5f)

				val newTickets = ObjectArrayList<Pair<Map.Entry<Vector2i, List<WorldObject>>, ServerChunk.ITicket>>()

				for (entry in copy) {
					val (pos, dependants) = entry
					if (dependants.isEmpty()) continue
					val ticket = world.permanentChunkTicket(world.geometry.chunkFromCell(pos)).await() ?: continue
					newTickets.add(entry to ticket)
					tickets.add(ticket)
				}

				coroutineScope {
					for ((entry, ticket) in newTickets) {
						val (pos, dependants) = entry

						launch {
							ticket.chunk.await()

							val findEntity = world.entityIndex.tileEntityAt(pos)

							if (findEntity is WorldObject) {
								// if entity exists, add it to working entities and find more not loaded entities
								populateWorking(findEntity)
							} else {
								// if entity does not exist - break connections
								for (dep in dependants) {
									for (node in dep.inputNodes) {
										node.removeConnectionsTo(pos)
									}

									for (node in dep.outputNodes) {
										node.removeConnectionsTo(pos)
									}
								}
							}
						}
					}
				}
			}

			// finally, update the network
			for (entity in entitySet) {
				for ((i, node) in entity.inputNodes.withIndex()) {
					val newState = node.connections.any { (pos, index) ->
						entities[pos]?.outputNodes?.getOrNull(index)?.state == true
					}

					if (newState != node.state) {
						try {
							node.state = newState
							entity.lua.invokeGlobal("onInputNodeChange", entity.lua.tableMapOf(NODE_KEY to i.toLong(), LEVEL_KEY to newState))
						} catch (err: Throwable) {
							LOGGER.error("Exception while updating wire state of $entity at ${entity.tilePosition} (input node index $i)", err)
						}
					}
				}
			}
		} finally {
			tickets.forEach { it.cancel() }
			entities.clear()
			entitySet.clear()
			unopenNodes = HashMap()
		}
	}

	fun tick(): Boolean {
		if (isTicking)
			return false

		isTicking = true

		world.eventLoop.scope.launch {
			try {
				tick0()
			} catch (err: Throwable) {
				LOGGER.error("Exception while updating wiring network", err)
			} finally {
				isTicking = false
			}
		}

		return true
	}

	private fun populateWorking(root: WorldObject) {
		if (entitySet.add(root)) {
			unopenNodes.remove(root.tilePosition)
			entities[root.tilePosition] = root

			for (node in root.inputNodes) {
				for (i in node.connections.indices) {
					val connection = node.connections[i]

					if (connection.entityLocation !in entities)
						unopenNodes.computeIfAbsent(connection.entityLocation) { ObjectArrayList(32) }.add(root)
				}
			}

			for (node in root.outputNodes) {
				for (i in node.connections.indices) {
					val connection = node.connections[i]

					if (connection.entityLocation !in entities)
						unopenNodes.computeIfAbsent(connection.entityLocation) { ObjectArrayList(32) }.add(root)
				}
			}
		}
	}

	companion object {
		val NODE_KEY: ByteString = ByteString.of("node")
		val LEVEL_KEY: ByteString = ByteString.of("level")
		private val LOGGER = LogManager.getLogger()
	}
}
