package ru.dbotthepony.kstarbound.network.syncher

import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.function.LongSupplier

// While I already have created my own syncher as part of Kommons (for minecraft mods),
// Starbound networking is done a bit differently.
// Most notably, networked variables employ interpolation and extrapolation,
// have different network data format when networking for first time and require
// strict field order when receiving them.

// Also, due to structure of networked delta data, most "lazy" optimizations can not
// be applied, such as dirty lists. If interpolation is enabled, all elements must either network their
// delta, or network "nothing changed, interpolate"

// But on second looks, versioning vs event has its own advantages, namely,
// IF data is updated way more frequent than it is sent to clients, then
// versioning approach is faster than event approach (because on each data update
// we don't need to notify all remote networkers about this)

// infrequent updates: Event
// very frequent updates: Polling with versioning
abstract class NetworkedElement {
	// Full store / load of the entire element.
	abstract fun readInitial(data: DataInputStream, isLegacy: Boolean)
	abstract fun writeInitial(data: DataOutputStream, isLegacy: Boolean)

	/**
	 * Read a delta written by writeNetDelta.  'interpolationTime' is the time in
	 * the future that data from this delta should be delayed and smoothed into,
	 * if interpolation is enabled.
	 */
	abstract fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean)

	/**
	 * Write all the state changes that have happened since (and including)
	 * fromVersion.  The normal way to use this would be to call writeDelta with
	 * the version at the time of the *last* call to writeDelta, + 1.  If
	 * fromVersion is 0, this will always write the full state.  Should return
	 * true if a delta was needed and was written to DataStream, false otherwise.
	 */
	abstract fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean)

	/**
	 * When extrapolating, it is important to notify when a delta WOULD have been
	 * received even if no deltas are produced, so no extrapolation takes place.
	 */
	abstract fun readBlankDelta(interpolationDelay: Double = 0.0)

	/**
	 * Enables interpolation mode.  If interpolation mode is enabled, then
	 * NetElements will delay presenting incoming delta data for the
	 * 'interpolationTime' parameter given in readNetDelta, and smooth between
	 * received values.  When interpolation is enabled, tickNetInterpolation must
	 * be periodically called to smooth values forward in time.  If
	 * extrapolationHint is given, this may be used as a hint for the amount of
	 * time to extrapolate forward if no deltas are received.
	 */
	abstract fun enableInterpolation(extrapolation: Double = 0.0)
	abstract fun disableInterpolation()
	abstract fun tickInterpolation(delta: Double)

	// A network of NetElements will have a shared monotinically increasing
	// NetElementVersion.  When elements are updated, they will mark the version
	// number at the time they are updated so that a delta can be constructed
	// that contains only changes since any past version.
	protected var version: Long = 0L
		set(value) {
			if (field != value) {
				require(value > field) { "Downgrading element version from $field to $value" }
				field = value
			}
		}

	protected fun currentVersion(): Long {
		return versionCounter?.asLong ?: version
	}

	open fun hasChangedSince(version: Long): Boolean {
		return this.version >= version
	}

	var versionCounter: LongSupplier? = null
		protected set

	open fun specifyVersioner(versionCounter: LongSupplier) {
		this.versionCounter = versionCounter
	}

	/**
	 * Marks this networked element dirty
	 */
	open fun bumpVersion() {
		version = versionCounter?.asLong ?: version
	}

	abstract class Passthrough : NetworkedElement() {
		protected abstract val parentElement: NetworkedElement

		override fun readInitial(data: DataInputStream, isLegacy: Boolean) {
			parentElement.readInitial(data, isLegacy)
		}

		override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
			parentElement.writeInitial(data, isLegacy)
		}

		override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) {
			parentElement.readDelta(data, interpolationDelay, isLegacy)
		}

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

		override fun readBlankDelta(interpolationDelay: Double) {
			parentElement.readBlankDelta(interpolationDelay)
		}

		override fun enableInterpolation(extrapolation: Double) {
			parentElement.enableInterpolation(extrapolation)
		}

		override fun disableInterpolation() {
			parentElement.disableInterpolation()
		}

		override fun tickInterpolation(delta: Double) {
			parentElement.tickInterpolation(delta)
		}

		override fun hasChangedSince(version: Long): Boolean {
			return super.hasChangedSince(version) || parentElement.hasChangedSince(version)
		}

		override fun specifyVersioner(versionCounter: LongSupplier) {
			super.specifyVersioner(versionCounter)
			parentElement.specifyVersioner(versionCounter)
		}

		override fun bumpVersion() {
			super.bumpVersion()
			parentElement.bumpVersion()
		}
	}
}
