package ru.dbotthepony.kstarbound.network.syncher

import ru.dbotthepony.kommons.io.readSignedVarLong
import ru.dbotthepony.kommons.io.readVarLong
import ru.dbotthepony.kommons.io.writeSignedVarLong
import ru.dbotthepony.kommons.io.writeVarLong
import ru.dbotthepony.kommons.util.Listenable
import ru.dbotthepony.kommons.util.ListenableDelegate
import ru.dbotthepony.kstarbound.math.Interpolator
import ru.dbotthepony.kstarbound.util.FloatSupplier
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.function.Consumer
import java.util.function.DoubleSupplier
import kotlin.math.absoluteValue
import kotlin.math.roundToLong

// works solely with doubles, but networks as either float, double or fixed point
class FloatingNetworkedElement(private var value: Double = 0.0, val ops: Ops, val legacyOps: Ops = ops, var interpolator: Interpolator = Interpolator.NearestMiddle) : NetworkedElement(), ListenableDelegate<Double>, DoubleSupplier {
	interface Ops {
		fun write(data: DataOutputStream, value: Double)
		fun read(data: DataInputStream): Double

		fun areDifferent(a: Double, b: Double): Boolean {
			// comparing doubles using direct comparison is bad for networking
			// return a != b
			// compare by epsilon
			return (a - b).absoluteValue > 0.0000001
		}
	}

	object DoubleOps : Ops {
		override fun write(data: DataOutputStream, value: Double) {
			data.writeDouble(value)
		}

		override fun read(data: DataInputStream): Double {
			return data.readDouble()
		}
	}

	object FloatOps : Ops {
		override fun write(data: DataOutputStream, value: Double) {
			data.writeFloat(value.toFloat())
		}

		override fun read(data: DataInputStream): Double {
			return data.readFloat().toDouble()
		}

		override fun areDifferent(a: Double, b: Double): Boolean {
			// comparing floats using direct comparison is bad for networking
			// return a != b
			// compare by epsilon
			return (a - b).absoluteValue > 0.00001f
		}
	}

	data class FixedPointOps(val base: Double) : Ops {
		override fun write(data: DataOutputStream, value: Double) {
			data.writeSignedVarLong((value / base).roundToLong())
		}

		override fun read(data: DataInputStream): Double {
			return data.readSignedVarLong() * base
		}

		override fun areDifferent(a: Double, b: Double): Boolean {
			return (a / base).roundToLong() != (b / base).roundToLong()
		}
	}

	private data class QueueEntry(val time: Double, val value: Double)
	private val queue = ArrayDeque<QueueEntry>()

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

	override fun toString(): String {
		return "FloatingNetworkedElement[$value, isInterpolating=$isInterpolating, currentTime=$currentTime]"
	}

	private val valueListeners = Listenable.Impl<Double>()

	override fun accept(t: Double) {
		if (t != value) {
			val old = value
			value = t

			if (ops.areDifferent(old, t))
				bumpVersion()

			if (isInterpolating) {
				queue.clear()
				queue.add(QueueEntry(currentTime, t))
			}

			valueListeners.accept(t)
		}
	}

	override fun addListener(listener: Consumer<Double>): Listenable.L {
		return valueListeners.addListener(listener)
	}

	override fun get(): Double {
		return value
	}

	override fun getAsDouble(): Double {
		return value
	}

	fun getAsFloat(): Float {
		return value.toFloat()
	}

	val float = FloatSupplier { getAsFloat() }

	override fun readInitial(data: DataInputStream, isLegacy: Boolean) {
		if (isLegacy) {
			value = legacyOps.read(data)
		} else {
			value = ops.read(data)
		}

		queue.clear()

		if (isInterpolating) {
			queue.add(QueueEntry(currentTime, value))
		}
	}

	override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
		if (queue.isNotEmpty())
			(if (isLegacy) legacyOps else ops).write(data, queue.last().value)
		else
			(if (isLegacy) legacyOps else ops).write(data, value)
	}

	override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) {
		val read = if (isLegacy) legacyOps.read(data) else ops.read(data)

		if (isInterpolating) {
			val realDelay = interpolationDelay + currentTime
			if (queue.last().time > realDelay)
				queue.clear()

			queue.add(QueueEntry(realDelay, read))
			value = interpolated()
			valueListeners.accept(value)
		} else {
			value = read
			valueListeners.accept(read)
		}

		bumpVersion()
	}

	override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) {
		writeInitial(data, isLegacy)
	}

	override fun readBlankDelta(interpolationDelay: Double) {
		if (isInterpolating) {
			val actual = interpolationDelay + currentTime
			val (last, lastPoint) = queue.last()

			if (actual < last)
				queue.clear()

			queue.add(QueueEntry(actual, lastPoint))

			val old = value
			value = interpolated()

			if (old != value) {
				valueListeners.accept(value)
			}
		}
	}

	override fun enableInterpolation(extrapolation: Double) {
		if (!isInterpolating) {
			isInterpolating = true
			queue.clear()
			queue.add(QueueEntry(currentTime, value))
		}

		this.extrapolation = extrapolation
	}

	override fun disableInterpolation() {
		if (isInterpolating) {
			isInterpolating = false

			if (queue.isNotEmpty()) {
				value = queue.last().value
			}

			queue.clear()
		}
	}

	override fun tickInterpolation(delta: Double) {
		require(delta >= 0.0) { "Negative interpolation delta: $delta" }
		currentTime += delta

		if (isInterpolating && queue.size >= 2) {
			while (queue.size > 2 && queue[1].time <= currentTime) {
				queue.removeFirst()
			}

			val old = value
			value = interpolated()

			if (value != old) {
				valueListeners.accept(value)
			}
		}
	}

	fun interpolated(time: Double = 0.0): Double {
		check(isInterpolating) { "Not interpolating" }
		check(queue.size >= 2) { "Interpolation queue is degenerate (only ${queue.size} points)" }

		val actualTime = time + currentTime

		if (actualTime < queue.first().time) {
			// extrapolate into past?
			val (time0, value0) = queue[0]
			val (time1, value1) = queue[1]

			var diff = ((actualTime - time0) / (time1 - time0)).coerceAtLeast(-extrapolation)

			if (diff.isNaN() || !diff.isFinite())
				diff = 0.0

			return interpolator.interpolate(diff, value0, value1)
		} else if (actualTime > queue.last().time) {
			// extrapolate into future
			val (time0, value0) = queue[queue.size - 2]
			val (time1, value1) = queue[queue.size - 1]

			var diff = ((actualTime - time1) / (time1 - time0)).coerceAtMost(extrapolation + 1.0)

			if (diff.isNaN() || !diff.isFinite())
				diff = 0.0

			return interpolator.interpolate(diff, value0, value1)
		} else {
			// normal interpolation
			for (i in 0 until queue.size - 1) {
				val (time0, value0) = queue[i]
				val (time1, value1) = queue[i + 1]

				if (actualTime in time0 .. time1) {
					var diff = (actualTime - time0) / (time1 - time0)

					if (diff.isNaN() || !diff.isFinite())
						diff = 0.0

					return interpolator.interpolate(diff, value0, value1)
				}
			}

			throw RuntimeException("unreachable code")
		}
	}

	companion object {
		/**
		 * Uses floats when asked to network for legacy protocol
		 */
		fun float(value: Double = 0.0): FloatingNetworkedElement {
			return FloatingNetworkedElement(value, DoubleOps, FloatOps)
		}

		fun double(value: Double = 0.0): FloatingNetworkedElement {
			return FloatingNetworkedElement(value, DoubleOps)
		}

		fun fixed(base: Double, value: Double = 0.0): FloatingNetworkedElement {
			return FloatingNetworkedElement(value, DoubleOps, FixedPointOps(base))
		}

		fun fixed2(base: Double, value: Double = 0.0): FloatingNetworkedElement {
			return FloatingNetworkedElement(value, FixedPointOps(base))
		}
	}
}
