package ru.dbotthepony.mc.otm.block.entity.cable

import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import ru.dbotthepony.mc.otm.capability.receiveEnergy
import ru.dbotthepony.mc.otm.core.ifPresentK
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.RelativeSide
import ru.dbotthepony.mc.otm.graph.GraphNodeList
import kotlin.math.ln

class EnergyCableGraph : GraphNodeList<EnergyCableBlockEntity.Node, EnergyCableGraph>() {
	val livelyNodes = ObjectOpenHashSet<EnergyCableBlockEntity.Node>()

	private val pathCache = Object2ObjectOpenHashMap<Pair<EnergyCableBlockEntity.Node, EnergyCableBlockEntity.Node>, Decimal?>()

	private class SearchNode(val node: EnergyCableBlockEntity.Node, target: EnergyCableBlockEntity.Node, var parent: SearchNode? = null) : Comparable<SearchNode> {
		var heuristics: Double = node.position.distSqr(target.position) * 0.0001 - ln(node.energyThroughput.coerceAtMost(Decimal.LONG_MAX_VALUE).toDouble())

		override fun compareTo(other: SearchNode): Int {
			return heuristics.compareTo(other.heuristics)
		}
	}

	fun invalidatePathCache() {
		pathCache.clear()
	}

	private fun getPath(a: EnergyCableBlockEntity.Node, b: EnergyCableBlockEntity.Node): Decimal? {
		if (!a.canTraverse || !b.canTraverse)
			return null

		val key = a to b

		if (key in pathCache)
			return pathCache[key]

		// no free paths available, try to find extra one
		// while this use A* algorithm, this is done purely for biasing search towards end point (to speed up search),
		// on small cable networks simple flooding will do just fine, if we consider overloaded cables as closed flood gates
		val openNodes = ArrayList<SearchNode>()
		val seenNodes = ObjectOpenHashSet<EnergyCableBlockEntity.Node>()

		openNodes.add(SearchNode(a, b))

		while (openNodes.isNotEmpty()) {
			val first = openNodes.min()
			openNodes.remove(first)

			if (first.node === b) {
				// solution found
				val solution = ArrayList<EnergyCableBlockEntity.Node>()

				var last = first.parent
				solution.add(first.node)

				while (last != null) {
					solution.add(last.node)
					last = last.parent
				}

				val calc = solution.minOf { it.energyThroughput }
				pathCache[key] = calc
				return calc
			} else {
				for (neighbour in first.node.neighboursView.values) {
					if (!seenNodes.add(neighbour) || !neighbour.canTraverse) continue
					openNodes.add(SearchNode(neighbour, b, first))
				}
			}
		}

		// solution does not exist
		pathCache[key] = null
		return null
	}

	override fun onNodeRemoved(node: EnergyCableBlockEntity.Node) {
		livelyNodes.remove(node)
		invalidatePathCache()
	}

	override fun onNodeAdded(node: EnergyCableBlockEntity.Node) {
		livelyNodes.add(node)
		invalidatePathCache()
	}

	fun receiveEnergy(howMuch: Decimal, simulate: Boolean, fromNode: EnergyCableBlockEntity.Node, fromSide: RelativeSide): Decimal {
		val itr = livelyNodes.iterator()
		var received = Decimal.ZERO
		var residue = howMuch.coerceAtMost(fromNode.energyThroughput)

		for (node in itr) {
			var hit = false

			for (side in node.sides.values) {
				if (side.isEnabled) {
					if (fromNode === node && side.side === fromSide) {
						hit = true
						continue
					}

					side.neighbour.get().ifPresentK {
						if (it !is EnergyCableBlockEntity.CableSide) {
							val limit = getPath(fromNode, node)
							hit = true

							if (limit != null) {
								val thisReceived = it.receiveEnergy(residue.coerceAtMost(limit), simulate)
								received += thisReceived
								residue -= thisReceived
								if (!residue.isPositive) return received
							}
						}
					}
				}
			}

			if (!hit) {
				itr.remove()
			}
		}

		return received
	}
}
