package ru.dbotthepony.kstarbound.network.syncher

import ru.dbotthepony.kommons.collect.ListenableMap
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.Listenable
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.concurrent.CopyOnWriteArrayList

/**
 * [isDumb] is responsible for specifying whenever legacy protocol networks entire map each time
 * instead of deltas
 *
 * [keyCodec] and [valueCodec] specify codecs as `native -> legacy` pair
 */
class NetworkedMap<K, V>(
	private val keyCodec: Pair<StreamCodec<K>, StreamCodec<K>>,
	private val valueCodec: Pair<StreamCodec<V>, StreamCodec<V>>,
	private val isDumb: Boolean = false,
	map: ListenableMap<K, V> = ListenableMap(),
	private val maxBacklogSize: Int = 100,
) : NetworkedElement(), MutableMap<K, V> by map {
	constructor(
		keyCodec: StreamCodec<K>,
		valueCodec: StreamCodec<V>,
		isDumb: Boolean = false,
		map: ListenableMap<K, V> = ListenableMap(),
		maxBacklogSize: Int = 100,
	) : this(keyCodec to keyCodec, valueCodec to valueCodec, isDumb, map, maxBacklogSize)

	constructor(
		keyCodec: StreamCodec<K>,
		valueCodec: Pair<StreamCodec<V>, StreamCodec<V>>,
		isDumb: Boolean = false,
		map: ListenableMap<K, V> = ListenableMap(),
		maxBacklogSize: Int = 100,
	) : this(keyCodec to keyCodec, valueCodec, isDumb, map, maxBacklogSize)

	constructor(
		keyCodec: Pair<StreamCodec<K>, StreamCodec<K>>,
		valueCodec: StreamCodec<V>,
		isDumb: Boolean = false,
		map: ListenableMap<K, V> = ListenableMap(),
		maxBacklogSize: Int = 100,
	) : this(keyCodec, valueCodec to valueCodec, isDumb, map, maxBacklogSize)

	init {
		map.addListener(object : ListenableMap.MapListener<K, V> {
			override fun onClear() {
				if (isReading > 0) return
				check(!isRemote) { "This map is not owned by this side" }

				// this is fragile (due to interpolation fuckery, we remove everything before applying delayed changes),
				// but let's hope it doesn't break
				queue.clear()
				backlog.add(currentVersion() to clearEntry)

				purgeBacklog()
				listeners.forEach { it.listener.onClear() }
			}

			override fun onValueAdded(key: K, value: V) {
				if (isReading > 0) return
				check(!isRemote) { "This map is not owned by this side" }
				backlog.add(currentVersion() to Entry(Action.ADD, KOptional(nativeKey.copy(key)), KOptional(nativeValue.copy(value))))
				purgeBacklog()
				listeners.forEach { it.listener.onValueAdded(key, value) }
			}

			override fun onValueRemoved(key: K, value: V) {
				if (isReading > 0) return
				check(!isRemote) { "This map is not owned by this side" }
				backlog.add(currentVersion() to Entry(Action.REMOVE, KOptional(nativeKey.copy(key)), KOptional()))
				purgeBacklog()
				listeners.forEach { it.listener.onValueRemoved(key, value) }
			}
		})
	}

	private val listeners = CopyOnWriteArrayList<Listener>()

	private inner class Listener(val listener: ListenableMap.MapListener<K, V>) : Listenable.L {
		init {
			listeners.add(this)
		}

		override fun remove() {
			listeners.remove(this)
		}
	}

	fun addListener(listener: ListenableMap.MapListener<K, V>): Listenable.L {
		return Listener(listener)
	}

	fun addListener(listener: Runnable): Listenable.L {
		return Listener(ListenableMap.RunnableAdapter(listener))
	}

	private val dumbCodec by lazy {
		StreamCodec.Map(keyCodec.second, valueCodec.second, ::HashMap)
	}

	private enum class Action {
		ADD, REMOVE, CLEAR;
	}

	private data class Entry<K, V>(val action: Action, val key: KOptional<K>, val value: KOptional<V>) {
		fun apply(map: MutableMap<K, V>) {
			when (action) {
				Action.ADD -> map[key.value] = value.value
				Action.REMOVE -> map.remove(key.value)
				Action.CLEAR -> map.clear()
			}
		}

		fun write(data: DataOutputStream, isLegacy: Boolean, self: NetworkedMap<K, V>) {
			data.writeByte(action.ordinal)

			if (isLegacy) {
				when (action) {
					Action.ADD -> {
						self.legacyKey.write(data, key.value)
						self.legacyValue.write(data, value.value)
					}

					Action.REMOVE -> {
						self.legacyKey.write(data, key.value)
					}

					Action.CLEAR -> {}
				}
			} else {
				when (action) {
					Action.ADD -> {
						self.nativeKey.write(data, key.value)
						self.nativeValue.write(data, value.value)
					}

					Action.REMOVE -> {
						self.nativeKey.write(data, key.value)
					}

					Action.CLEAR -> {}
				}
			}
		}
	}

	private fun readLegacyEntry(data: DataInputStream): Entry<K, V> {
		return when (Action.entries[data.readUnsignedByte()]) {
			Action.ADD -> Entry(Action.ADD, KOptional(legacyKey.read(data)), KOptional(legacyValue.read(data)))
			Action.REMOVE -> Entry(Action.REMOVE, KOptional(legacyKey.read(data)), KOptional())
			Action.CLEAR -> clearEntry
		}
	}

	private fun readNativeEntry(data: DataInputStream): Entry<K, V> {
		return when (Action.entries[data.readUnsignedByte()]) {
			Action.ADD -> Entry(Action.ADD, KOptional(nativeKey.read(data)), KOptional(nativeValue.read(data)))
			Action.REMOVE -> Entry(Action.REMOVE, KOptional(nativeKey.read(data)), KOptional())
			Action.CLEAR -> clearEntry
		}
	}

	private val clearEntry = Entry<K, V>(Action.CLEAR, KOptional(), KOptional())
	private val backlog = ArrayDeque<Pair<Long, Entry<K, V>>>()

	private val queue = InterpolationQueue<Entry<K, V>> {
		isReading++
		it.apply(this)
		isReading--
	}

	private var isReading = 0
	private var isRemote = false
	private var isInterpolating = false

	val legacyKey get() = keyCodec.second
	val nativeKey get() = keyCodec.first
	val legacyValue get() = valueCodec.second
	val nativeValue get() = valueCodec.first

	private fun purgeBacklog() {
		while (backlog.size >= maxBacklogSize) {
			backlog.removeFirst()
		}
	}

	override fun toString(): String {
		return "NetworkedMap[keys = ${keys.size}]"
	}

	override fun readInitial(data: DataInputStream, isLegacy: Boolean) {
		try {
			isRemote = true
			isReading++

			backlog.clear()
			queue.clear()
			clear()

			backlog.add(currentVersion() to clearEntry)

			if (isDumb && isLegacy) {
				val readMap = dumbCodec.read(data)

				for ((k, v) in readMap) {
					val action = Entry(Action.ADD, KOptional(k), KOptional(v))
					backlog.add(currentVersion() to action)
					action.apply(this)
				}
			} else {
				val values = data.readVarInt()

				if (isLegacy) {
					for (i in 0 until values) {
						val action = readLegacyEntry(data)
						backlog.add(currentVersion() to action)
						action.apply(this)
					}
				} else {
					for (i in 0 until values) {
						val action = readNativeEntry(data)
						backlog.add(currentVersion() to action)
						action.apply(this)
					}
				}
			}

			purgeBacklog()
		} finally {
			isReading--
		}
	}

	override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
		if (isDumb && isLegacy) {
			val construct = HashMap<K, V>(size + queue.size)

			for ((k, v) in entries) {
				construct[k] = v
			}

			for (v in queue) {
				v.apply(construct)
			}

			dumbCodec.write(data, construct)
		} else {
			data.writeVarInt(size + queue.size)

			for ((k, v) in entries) {
				Entry(Action.ADD, KOptional(k), KOptional(v)).write(data, isLegacy, this)
			}

			for (v in queue) {
				v.write(data, isLegacy, this)
			}
		}
	}

	override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) {
		if (isDumb && isLegacy) {
			readInitial(data, true)
			return
		}

		try {
			isReading++

			while (true) {
				when (val action = data.readUnsignedByte()) {
					0 -> break
					1 -> readInitial(data, isLegacy)
					2 -> {
						val change = if (isLegacy) readLegacyEntry(data) else readNativeEntry(data)
						backlog.add(currentVersion() to change)

						if (isInterpolating) {
							queue.push(change, interpolationDelay)
						} else {
							change.apply(this)
						}
					}

					else -> throw IllegalArgumentException("Invalid delta change type $action")
				}
			}

			purgeBacklog()
		} finally {
			isReading--
		}
	}

	override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) {
		if (isDumb && isLegacy) {
			writeInitial(data, true)
			return
		}

		if (remoteVersion < backlog.first().first) {
			// we fell way behind, serialize entire state
			data.writeByte(1)
			writeInitial(data, isLegacy)
			data.writeByte(0)
		} else {
			for ((version, entry) in backlog) {
				if (version >= remoteVersion) {
					data.writeByte(2)
					entry.write(data, isLegacy, this)
				}
			}

			data.writeByte(0)
		}
	}

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

	override fun readBlankDelta(interpolationDelay: Double) {}

	override fun enableInterpolation(extrapolation: Double) {
		isInterpolating = true
	}

	override fun disableInterpolation() {
		if (isInterpolating) {
			isInterpolating = false
			queue.clear()
			purgeBacklog()
		}
	}

	override fun tickInterpolation(delta: Double) {
		queue.tick(delta)
	}
}
