package ru.dbotthepony.kstarbound.network.syncher

import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeVarInt
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.function.Consumer
import java.util.function.LongSupplier

class NetworkedGroup() : NetworkedElement() {
	constructor(element: NetworkedElement, vararg rest: NetworkedElement) : this() {
		add(element)
		rest.forEach { add(it) }
	}

	// element -> propagateInterpolation
	private val elements = ArrayList<Pair<NetworkedElement, Boolean>>()

	var isInterpolating = false
		private set
	var extrapolation = 0.0
		private set

	fun clear() {
		elements.clear()
	}

	override fun toString(): String {
		if (elements.isEmpty())
			return "NetworkedGroup[]"

		return "NetworkedGroup[\n${elements.joinToString("\n") { "\t" + it.first.toString() }}]"
	}

	override fun specifyVersioner(versionCounter: LongSupplier) {
		super.specifyVersioner(versionCounter)
		elements.forEach { it.first.specifyVersioner(versionCounter) }
	}

	override fun enableInterpolation(extrapolation: Double) {
		if (!isInterpolating || extrapolation != this.extrapolation) {
			isInterpolating = true
			this.extrapolation = extrapolation
			elements.forEach { it.first.enableInterpolation(extrapolation) }
		}
	}

	override fun disableInterpolation() {
		if (isInterpolating) {
			isInterpolating = false
			extrapolation = 0.0
			elements.forEach { it.first.disableInterpolation() }
		}
	}

	override fun tickInterpolation(delta: Double) {
		elements.forEach { it.first.tickInterpolation(delta) }
	}

	private var trap: Consumer<DataInputStream>? = null

	fun putTrap(trap: Consumer<DataInputStream>) {
		this.trap = trap
	}

	private fun setupElement(element: NetworkedElement, propagateInterpolation: Boolean) {
		if (propagateInterpolation) {
			if (isInterpolating)
				element.enableInterpolation(extrapolation)
			else
				element.disableInterpolation()
		}

		if (versionCounter != null) {
			element.specifyVersioner(versionCounter!!)
		}
	}

	fun <E : NetworkedElement> add(element: E, propagateInterpolation: Boolean = true): E {
		require(elements.none { it.first === element }) { "Already has element $element in $this" }
		elements.add(element to propagateInterpolation)
		setupElement(element, propagateInterpolation)
		return element
	}

	fun replace(find: NetworkedElement, replace: NetworkedElement) {
		if (find === replace) return
		val index = elements.indexOfFirst { it.first === find }
		check(index != -1) { "Unable to find $find in $this" }
		setupElement(replace, elements[index].second)
		elements[index] = replace to elements[index].second
	}

	override fun readInitial(data: DataInputStream, isLegacy: Boolean) {
		trap?.accept(data)
		elements.forEach { it.first.readInitial(data, isLegacy) }
	}

	override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
		elements.forEach { it.first.writeInitial(data, isLegacy) }
	}

	override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) {
		check(elements.isNotEmpty()) { "No networked elements in this group" }

		if (elements.size == 1) {
			elements[0].first.readDelta(data, interpolationDelay, isLegacy)
		} else {
			var nextIndex = data.readVarInt()

			// here goes more funk - when interpolating,
			// elements missing from network message are interpolated

			if (isInterpolating) {
				// when interpolating, we can't just sparsingly read elements,
				// since elements which are absent from delta must be updated too
				// Things get ugly

				for ((i, element) in elements.withIndex()) {
					if (nextIndex == 0 || i < nextIndex - 1) {
						element.first.readBlankDelta(interpolationDelay)
					} else if (i == nextIndex - 1) {
						element.first.readDelta(data, interpolationDelay, isLegacy)
						nextIndex = data.readVarInt()
					} else {
						throw IllegalStateException("Networked element group indexes were written out of order")
					}
				}
			} else {
				while (nextIndex != 0) {
					val element = elements.getOrNull(nextIndex - 1) ?: throw NoSuchElementException("Unknown networked element with index ${nextIndex - 1}!")
					element.first.readDelta(data, interpolationDelay, isLegacy)
					nextIndex = data.readVarInt()
				}
			}
		}
	}

	override fun readBlankDelta(interpolationDelay: Double) {
		if (isInterpolating) {
			elements.forEach { it.first.readBlankDelta(interpolationDelay) }
		}
	}

	override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) {
		check(elements.isNotEmpty()) { "No networked elements in this group" }

		// here is where it gets funky, data structure
		// is different for when there is only one element in group
		// or multiple

		if (elements.size == 1) {
			// one element - pass through
			elements[0].first.writeDelta(data, remoteVersion, isLegacy)
		} else {
			// otherwise, sequentially scan elements for updates
			// and if element needs updating, write element's index as variable length integer;
			// then write element itself

			for ((i, element) in elements.withIndex()) {
				if (element.first.hasChangedSince(remoteVersion)) {
					data.writeVarInt(i + 1)
					element.first.writeDelta(data, remoteVersion, isLegacy)
				}
			}

			data.writeVarInt(0)
		}
	}

	override fun hasChangedSince(version: Long): Boolean {
		return elements.any { it.first.hasChangedSince(version) }
	}

	override fun bumpVersion() {
		elements.forEach { it.first.bumpVersion() }
	}
}
