package ru.dbotthepony.mc.otm.network

import io.netty.buffer.ByteBuf
import it.unimi.dsi.fastutil.bytes.ByteArrayList
import net.minecraft.core.RegistryAccess
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.codec.StreamCodec
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.writeVarInt
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.NoSuchElementException

interface MatteryStreamCodec<in S : ByteBuf, V> : StreamCodec<@UnsafeVariance S, V> {
	fun copy(value: V): V {
		return value
	}

	fun compare(a: V, b: V): Boolean {
		return a == b
	}

	// ignore kotlin compiler warning; StreamCodec<> has wrong package-wide nullability annotation
	override fun decode(stream: S): V
	override fun encode(stream: S, value: V)

	class Of<in S : ByteBuf, V>(private val writer: S.(V) -> Unit, private val reader: S.() -> V) : MatteryStreamCodec<S, V> {
		override fun decode(stream: S): V {
			return reader(stream)
		}

		override fun encode(stream: S, value: V) {
			return writer(stream, value)
		}
	}

	abstract class AbstractPair<in S : ByteBuf, A, B, P>(private val first: MatteryStreamCodec<S, A>, private val second: MatteryStreamCodec<S, B>) : MatteryStreamCodec<S, P> {
		protected abstract fun getFirst(value: P): A
		protected abstract fun getSecond(value: P): B
		protected abstract fun construct(a: A, b: B): P

		override fun decode(stream: S): P {
			return construct(first.decode(stream), second.decode(stream))
		}

		override fun encode(stream: S, value: P) {
			first.encode(stream, getFirst(value))
			second.encode(stream, getSecond(value))
		}

		override fun copy(value: P): P {
			val a = first.copy(getFirst(value))
			val b = second.copy(getSecond(value))

			if (a === getFirst(value) && b === getSecond(value))
				return value

			return construct(a, b)
		}

		override fun compare(a: P, b: P): Boolean {
			return first.compare(getFirst(a), getFirst(b)) && second.compare(getSecond(a), getSecond(b))
		}
	}

	class Pair<in S : ByteBuf, A, B>(first: MatteryStreamCodec<S, A>, second: MatteryStreamCodec<S, B>) : AbstractPair<S, A, B, kotlin.Pair<A, B>>(first, second) {
		override fun getFirst(value: kotlin.Pair<A, B>): A {
			return value.first
		}

		override fun getSecond(value: kotlin.Pair<A, B>): B {
			return value.second
		}

		override fun construct(a: A, b: B): kotlin.Pair<A, B> {
			return a to b
		}
	}

	class MapEntry<in S : ByteBuf, A, B>(first: MatteryStreamCodec<S, A>, second: MatteryStreamCodec<S, B>) : AbstractPair<S, A, B, Map.Entry<A, B>>(first, second) {
		override fun getFirst(value: Map.Entry<A, B>): A {
			return value.key
		}

		override fun getSecond(value: Map.Entry<A, B>): B {
			return value.value
		}

		override fun construct(a: A, b: B): Map.Entry<A, B> {
			return object : Map.Entry<A, B> {
				override val key: A = a
				override val value: B = b
			}
		}
	}

	class Wrapper<in S : ByteBuf, V>(parent: StreamCodec<S, V>) : MatteryStreamCodec<S, V>, StreamCodec<@UnsafeVariance S, V> by parent

	class Nullable<in S : ByteBuf, V>(val parent: MatteryStreamCodec<S, V>) : MatteryStreamCodec<S, V?> {
		override fun decode(stream: S): V? {
			return if (!stream.readBoolean()) null else parent.decode(stream)
		}

		override fun encode(stream: S, value: V?) {
			if (value === null)
				stream.writeBoolean(false)
			else {
				stream.writeBoolean(true)
				parent.encode(stream, value)
			}
		}

		override fun copy(value: V?): V? {
			return if (value === null) null else parent.copy(value)
		}

		override fun compare(a: V?, b: V?): Boolean {
			if (a === null && b === null) return true
			if (a === null || b === null) return false
			return parent.compare(a, b)
		}
	}

	class Optional<in S : ByteBuf, V : Any>(val parent: MatteryStreamCodec<S, V>) : MatteryStreamCodec<S, java.util.Optional<V>> {
		override fun decode(stream: S): java.util.Optional<V> {
			return if (!stream.readBoolean()) java.util.Optional.empty() else java.util.Optional.of(parent.decode(stream))
		}

		override fun encode(stream: S, value: java.util.Optional<V>) {
			if (value.isEmpty)
				stream.writeBoolean(false)
			else {
				stream.writeBoolean(true)
				parent.encode(stream, value.get())
			}
		}

		override fun copy(value: java.util.Optional<V>): java.util.Optional<V> {
			return value.map(parent::copy)
		}

		override fun compare(a: java.util.Optional<V>, b: java.util.Optional<V>): Boolean {
			if (a.isEmpty && b.isEmpty) return true
			if (a.isEmpty || b.isEmpty) return false
			return parent.compare(a.get(), b.get())
		}
	}

	class Collection<in S : FriendlyByteBuf, E, C : MutableCollection<E>>(val elementCodec: MatteryStreamCodec<S, E>, val collectionFactory: (Int) -> C) : MatteryStreamCodec<S, C> {
		override fun decode(stream: S): C {
			val size = stream.readVarInt()

			if (size <= 0) {
				return collectionFactory.invoke(0)
			}

			val collection = collectionFactory.invoke(size)

			for (i in 0 until size) {
				collection.add(elementCodec.decode(stream))
			}

			return collection
		}

		override fun encode(stream: S, value: C) {
			stream.writeVarInt(value.size)
			value.forEach { elementCodec.encode(stream, it) }
		}

		override fun copy(value: C): C {
			val new = collectionFactory.invoke(value.size)
			value.forEach { new.add(elementCodec.copy(it)) }
			return new
		}
	}

	class Enum<in S : FriendlyByteBuf, V : kotlin.Enum<V>>(clazz: Class<out V>) : MatteryStreamCodec<S, V> {
		val clazz = searchClass(clazz)
		val values: List<V> = listOf(*this.clazz.enumConstants!!)
		val valuesMap = values.associateBy { it.name }

		override fun decode(stream: S): V {
			val id = stream.readVarInt()
			return values.getOrNull(id) ?: throw NoSuchElementException("No such enum with index $id")
		}

		override fun encode(stream: S, value: V) {
			stream.writeVarInt(value.ordinal)
		}

		override fun copy(value: V): V {
			return value
		}

		override fun compare(a: V, b: V): Boolean {
			return a === b
		}

		companion object {
			/**
			 * FIXME: enums with abstract methods which get compiled to subclasses, whose DO NOT expose "parent's" enum constants array
			 *
			 * is there an already existing solution?
			 */
			fun <V : kotlin.Enum<V>> searchClass(clazz: Class<out V>): Class<out V> {
				var search: Class<*> = clazz

				while (search.enumConstants == null && search.superclass != null) {
					search = search.superclass
				}

				if (search.enumConstants == null) {
					throw ClassCastException("$clazz does not represent an enum or enum subclass")
				}

				return search as Class<out V>
			}
		}
	}
}
