package ru.dbotthepony.kstarbound.network.syncher

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.kstarbound.collect.RandomListIterator
import ru.dbotthepony.kstarbound.collect.RandomSubList
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.concurrent.CopyOnWriteArrayList

// original engine does not have "networked list", so it is always networked
// the dumb way on legacy protocol
class NetworkedList<E>(
	val codec: StreamCodec<E>,
	val legacyCodec: StreamCodec<E> = codec,
	private val maxBacklogSize: Int = 100,
	private val elementsFactory: (Int) -> MutableList<E> = ::ArrayList
) : NetworkedElement(), MutableList<E> {
	private val backlog = ArrayDeque<Pair<Long, Entry<E>>>()
	private val elements = elementsFactory(10)
	private val queue = InterpolationQueue<Entry<E>> { it.apply(elements) }

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

	private data class Entry<E>(val type: Type, val index: Int, val value: KOptional<E>) {
		constructor(index: Int) : this(Type.REMOVE, index, KOptional())
		constructor(index: Int, value: E) : this(Type.REMOVE, index, KOptional(value))

		fun apply(list: MutableList<E>) {
			when (type) {
				Type.ADD -> list.add(index, value.value)
				Type.REMOVE -> list.removeAt(index)
				Type.CLEAR -> list.clear()
			}
		}
	}

	private val clearEntry = Entry<E>(Type.CLEAR, 0, KOptional())
	private var isInterpolating = false
	private var isRemote = false
	private val listeners = CopyOnWriteArrayList<Runnable>()

	fun addListener(listener: Runnable) {
		listeners.add(listener)
	}

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

	private fun latestState(): List<E> {
		if (queue.isEmpty) {
			return elements
		} else {
			val copy = elementsFactory(elements.size)

			for (v in elements)
				copy.add(v)

			queue.forEach { it.apply(copy) }
			return copy
		}
	}

	override fun readInitial(data: DataInputStream, isLegacy: Boolean) {
		isRemote = true
		backlog.clear()
		backlog.add(currentVersion() to clearEntry)
		queue.clear()
		elements.clear()

		val count = data.readVarInt()

		if (isLegacy) {
			for (i in 0 until count) {
				val read = legacyCodec.read(data)
				elements.add(read)
				backlog.add(currentVersion() to Entry(elements.size - 1, read))
			}
		} else {
			for (i in 0 until count) {
				val read = codec.read(data)
				elements.add(read)
				backlog.add(currentVersion() to Entry(elements.size - 1, read))
			}
		}

		purgeBacklog()
		listeners.forEach { it.run() }
	}

	override fun writeInitial(data: DataOutputStream, isLegacy: Boolean) {
		val latest = latestState()
		data.writeVarInt(latest.size)

		if (isLegacy) {
			latest.forEach { legacyCodec.write(data, it) }
		} else {
			latest.forEach { codec.write(data, it) }
		}
	}

	override fun readDelta(data: DataInputStream, interpolationDelay: Double, isLegacy: Boolean) {
		isRemote = true

		if (isLegacy) {
			readInitial(data, true)
		} else {
			if (data.readBoolean()) {
				var action = data.readUnsignedByte()

				while (action != 0) {
					val entry = when (Type.entries[action - 1]) {
						Type.ADD -> Entry(data.readVarInt(), codec.read(data))
						Type.REMOVE -> Entry(data.readVarInt())
						Type.CLEAR -> clearEntry
					}

					if (isInterpolating) {
						queue.push(entry, interpolationDelay)
					} else {
						entry.apply(elements)
					}

					backlog.add(currentVersion() to entry)
					action = data.readUnsignedByte()
				}

				purgeBacklog()
				listeners.forEach { it.run() }
			} else {
				readInitial(data, false)
			}
		}
	}

	override fun writeDelta(data: DataOutputStream, remoteVersion: Long, isLegacy: Boolean) {
		if (isLegacy) {
			writeInitial(data, true)
		} else if (backlog.isNotEmpty() && remoteVersion < backlog.first().first) {
			data.writeBoolean(false)
			writeInitial(data, false)
		} else {
			data.writeBoolean(true)

			for ((version, entry) in backlog) {
				if (version >= remoteVersion) {
					data.writeByte(entry.type.ordinal + 1)

					when (entry.type) {
						Type.ADD -> { data.writeVarInt(entry.index); codec.write(data, entry.value.value) }
						Type.REMOVE -> { data.writeVarInt(entry.index) }
						Type.CLEAR -> {}
					}
				}
			}

			data.writeByte(0)
		}
	}

	override fun readBlankDelta(interpolationDelay: Double) {}

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

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

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

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

	override val size: Int
		get() = elements.size

	override fun contains(element: E): Boolean {
		return elements.contains(element)
	}

	override fun containsAll(elements: Collection<E>): Boolean {
		return this.elements.containsAll(elements)
	}

	override fun get(index: Int): E {
		return elements[index]
	}

	override fun indexOf(element: E): Int {
		return elements.indexOf(element)
	}

	override fun isEmpty(): Boolean {
		return elements.isEmpty()
	}

	override fun iterator(): MutableIterator<E> {
		return listIterator()
	}

	override fun lastIndexOf(element: E): Int {
		return elements.lastIndexOf(element)
	}

	override fun add(element: E): Boolean {
		add(size, element)
		return true
	}

	override fun add(index: Int, element: E) {
		check(!isRemote) { "List is not owned by this side" }
		elements.add(index, element)
		backlog.add(currentVersion() to Entry(index, element))
		purgeBacklog()
		listeners.forEach { it.run() }
	}

	override fun addAll(index: Int, elements: Collection<E>): Boolean {
		var newIndex = index
		elements.forEach { add(newIndex++, it) }
		return true
	}

	override fun addAll(elements: Collection<E>): Boolean {
		return addAll(size, elements)
	}

	override fun clear() {
		check(!isRemote) { "List is not owned by this side" }
		backlog.clear()
		backlog.add(currentVersion() to clearEntry)
		elements.clear()
		listeners.forEach { it.run() }
	}

	override fun listIterator(): MutableListIterator<E> {
		return listIterator(0)
	}

	override fun listIterator(index: Int): MutableListIterator<E> {
		return RandomListIterator(this, index)
	}

	override fun remove(element: E): Boolean {
		check(!isRemote) { "List is not owned by this side" }
		val indexOf = elements.indexOf(element)

		if (indexOf == -1)
			return false

		removeAt(indexOf)
		return true
	}

	override fun removeAll(elements: Collection<E>): Boolean {
		check(!isRemote) { "List is not owned by this side" }
		var any = false
		elements.forEach { any = remove(it) || any }
		return any
	}

	override fun removeAt(index: Int): E {
		check(!isRemote) { "List is not owned by this side" }
		val element = elements.removeAt(index)
		backlog.add(currentVersion() to Entry(index))
		purgeBacklog()
		listeners.forEach { it.run() }
		return element
	}

	override fun retainAll(elements: Collection<E>): Boolean {
		check(!isRemote) { "List is not owned by this side" }
		val itr = iterator()
		var modified = false

		for (v in itr) {
			if (v !in elements) {
				itr.remove()
				modified = true
			}
		}

		return modified
	}

	override fun set(index: Int, element: E): E {
		check(!isRemote) { "List is not owned by this side" }
		val old = elements.set(index, element)
		backlog.add(currentVersion() to Entry(index, element))
		purgeBacklog()
		listeners.forEach { it.run() }
		return old
	}

	override fun subList(fromIndex: Int, toIndex: Int): MutableList<E> {
		return RandomSubList(this, fromIndex, toIndex)
	}

	override fun equals(other: Any?): Boolean {
		if (other === this) return true
		if (other !is List<*>) return false

		val e1: ListIterator<E> = listIterator()
		val e2 = other.listIterator()

		while (e1.hasNext() && e2.hasNext()) {
			val o1 = e1.next()
			val o2 = e2.next()
			if (!(if (o1 == null) o2 == null else o1 == o2)) return false
		}

		return !(e1.hasNext() || e2.hasNext())
	}

	override fun hashCode(): Int {
		var hashCode = 1
		for (e in this) hashCode = 31 * hashCode + e.hashCode()
		return hashCode
	}

	override fun toString(): String {
		return "NetworkedList[${joinToString()}]"
	}
}
