package ru.dbotthepony.kstarbound.network.syncher

import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.TypeAdapter
import ru.dbotthepony.kommons.io.BinaryStringCodec
import ru.dbotthepony.kommons.io.BooleanValueCodec
import ru.dbotthepony.kommons.io.IntValueCodec
import ru.dbotthepony.kommons.io.RGBACodec
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.UnsignedVarIntCodec
import ru.dbotthepony.kommons.io.UnsignedVarLongCodec
import ru.dbotthepony.kommons.io.VarIntValueCodec
import ru.dbotthepony.kommons.io.VarLongValueCodec
import ru.dbotthepony.kommons.io.Vector2dCodec
import ru.dbotthepony.kommons.io.Vector2fCodec
import ru.dbotthepony.kommons.io.koptional
import ru.dbotthepony.kommons.io.map
import ru.dbotthepony.kommons.io.readByteArray
import ru.dbotthepony.kommons.io.readVarInt
import ru.dbotthepony.kommons.io.readVarLong
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeShort
import ru.dbotthepony.kommons.io.writeVarInt
import ru.dbotthepony.kommons.io.writeVarLong
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.item.ItemDescriptor
import ru.dbotthepony.kstarbound.defs.world.SkyType
import ru.dbotthepony.kstarbound.io.readAABB
import ru.dbotthepony.kstarbound.io.readAABBLegacy
import ru.dbotthepony.kstarbound.io.readAABBLegacyOptional
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.writeAABB
import ru.dbotthepony.kstarbound.io.writeAABBLegacy
import ru.dbotthepony.kstarbound.io.writeAABBLegacyOptional
import ru.dbotthepony.kstarbound.item.ItemStack
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.readJsonElement
import ru.dbotthepony.kstarbound.json.readJsonObject
import ru.dbotthepony.kstarbound.json.writeJsonElement
import ru.dbotthepony.kstarbound.json.writeJsonObject
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.InputStream
import java.io.OutputStream

fun <TYPE> BasicNetworkedElement(value: TYPE, codec: StreamCodec<TYPE>): BasicNetworkedElement<TYPE, TYPE> {
	return BasicNetworkedElement(value, codec, codec, { it }, { it })
}

val ByteArrayCodec = StreamCodec.Impl(DataInputStream::readByteArray, DataOutputStream::writeByteArray)
val InternedStringCodec = StreamCodec.Impl(DataInputStream::readInternedString, DataOutputStream::writeBinaryString)

val AABBCodecLegacy = StreamCodec.Impl(DataInputStream::readAABBLegacy, DataOutputStream::writeAABBLegacy)
val AABBCodecLegacyOptional = StreamCodec.Impl(DataInputStream::readAABBLegacyOptional, DataOutputStream::writeAABBLegacyOptional)
val AABBCodecNative = StreamCodec.Impl(DataInputStream::readAABB, DataOutputStream::writeAABB)

val UnsignedShortCodec = StreamCodec.Impl(DataInputStream::readUnsignedShort, DataOutputStream::writeShort)

val ValidatingBooleanCodec = StreamCodec.Impl({
	when (val read = it.readUnsignedByte()) {
		0 -> false
		1 -> true
		else -> throw IllegalStateException("Expected to read proper boolean from stream, but incoming byte was: $read")
	}
},  DataOutputStream::writeBoolean)

// networking size_t...
// god help us all
val SizeTCodec = StreamCodec.Impl({ stream -> stream.readVarLong().let { if (it == 0L) -1L else it - 1L } }, { stream, value -> if (value in 0L..<Long.MAX_VALUE) stream.writeVarLong(value + 1L) else stream.writeVarLong(0L) })

fun InputStream.readPointer(): Long {
	val read = readVarLong()

	if (read == 0L) {
		return -1L
	} else {
		return read - 1L
	}
}

fun OutputStream.writePointer(value: Int) {
	if (value < 0L) {
		writeVarLong(0L)
	} else {
		writeVarLong(value + 1L)
	}
}

fun OutputStream.writePointer(value: Long) {
	if (value in 0L..<Long.MAX_VALUE) {
		writeVarLong(value + 1L)
	} else {
		writeVarLong(0L)
	}
}

fun networkedFloat(value: Double = 0.0) = FloatingNetworkedElement.float(value)
fun networkedDouble(value: Double = 0.0) = FloatingNetworkedElement.double(value)
fun networkedFixedPoint(base: Double, value: Double = 0.0) = FloatingNetworkedElement.fixed(base, value)

/**
 * also uses fixed point on native protocol
 */
fun networkedFixedPoint2(base: Double, value: Double = 0.0) = FloatingNetworkedElement.fixed(base, value)
fun networkedSignedInt(value: Int = 0) = BasicNetworkedElement(value, VarIntValueCodec)
fun networkedUnsignedInt(value: Int = 0) = BasicNetworkedElement(value, UnsignedVarIntCodec)
fun networkedSignedLong(value: Long = 0L) = BasicNetworkedElement(value, VarLongValueCodec)
fun networkedUnsignedLong(value: Long = 0L) = BasicNetworkedElement(value, UnsignedVarLongCodec)
fun networkedBoolean(value: Boolean = false) = BasicNetworkedElement(value, ValidatingBooleanCodec)
fun networkedPointer(value: Long = 0L) = BasicNetworkedElement(value, SizeTCodec)
fun networkedVec2f(value: Vector2d = Vector2d.ZERO) = BasicNetworkedElement(value, Vector2dCodec, Vector2fCodec, { it.toFloatVector() }, { it.toDoubleVector() })
fun networkedBytes(value: ByteArray = ByteArray(0)) = BasicNetworkedElement(value, ByteArrayCodec)

fun <T> networkedData(value: T, codec: StreamCodec<T>) = BasicNetworkedElement(value, codec)
fun <T> networkedData(value: T, codec: StreamCodec<T>, legacyCodec: StreamCodec<T>) = BasicNetworkedElement(value, codec, legacyCodec, { it }, { it })

fun networkedAABB(value: AABB = AABB.ZERO) = networkedData(value, AABBCodecNative, AABBCodecLegacy)
fun networkedAABBNullable(value: KOptional<AABB> = KOptional()) = networkedData(value, AABBCodecNative.koptional(), AABBCodecLegacyOptional)

// this is ugly because of invariant generics, but we must bear with it.
fun <E> networkedList(codec: StreamCodec<E>): BasicNetworkedElement<List<E>, List<E>> = networkedData(ArrayList(), StreamCodec.Collection(codec, ::ArrayList)) as BasicNetworkedElement<List<E>, List<E>>
fun <E> networkedList(codec: StreamCodec<E>, legacyCodec: StreamCodec<E>): BasicNetworkedElement<List<E>, List<E>> = networkedData(ArrayList(), StreamCodec.Collection(codec, ::ArrayList), StreamCodec.Collection(legacyCodec, ::ArrayList)) as BasicNetworkedElement<List<E>, List<E>>
fun networkedItem(value: ItemStack = ItemStack.EMPTY) = NetworkedItemStack(value)
fun networkedStatefulItem(value: ItemStack = ItemStack.EMPTY) = NetworkedStatefulItemStack(value)

fun networkedEventCounter() = EventCounterElement()
fun <S> networkedSignals(codec: StreamCodec<S>) = NetworkedSignal(codec)
fun <S> networkedSignals(codec: StreamCodec<S>, maxSize: Int) = NetworkedSignal(codec, maxSize)
fun networkedString(value: String = "") = BasicNetworkedElement(value, InternedStringCodec)

fun networkedPoly(value: Poly) = networkedData(value, Poly.CODEC, Poly.LEGACY_CODEC)

inline fun <reified T> networkedJson(value: T, adapter: TypeAdapter<T> = Starbound.gson.getAdapter(T::class.java), legacyIsArray: Boolean = true): BasicNetworkedElement<T, *> {
	if (legacyIsArray) {
		return BasicNetworkedElement(value, JsonCodec(adapter), JsonCodec(adapter, true), { it }, { it })
	} else {
		return BasicNetworkedElement(value, JsonCodec(adapter))
	}
}

val JsonElementCodec = StreamCodec.Impl(DataInputStream::readJsonElement, DataOutputStream::writeJsonElement)
val JsonObjectCodec = StreamCodec.Impl(DataInputStream::readJsonObject, DataOutputStream::writeJsonObject)
fun networkedJsonElement(value: JsonElement = JsonNull.INSTANCE) = networkedData(value, JsonElementCodec)
fun networkedJsonObject(value: JsonObject) = networkedData(value, JsonObjectCodec)

fun <T> nativeCodec(reader: DataInputStream.(Boolean) -> T, writer: T.(DataOutputStream, Boolean) -> Unit): StreamCodec<T> {
	return StreamCodec.Impl({ reader(it, false) }, { a, b -> writer(b, a, false) })
}

fun <T> legacyCodec(reader: DataInputStream.(Boolean) -> T, writer: T.(DataOutputStream, Boolean) -> Unit): StreamCodec<T> {
	return StreamCodec.Impl({ reader(it, true) }, { a, b -> writer(b, a, true) })
}

fun networkedColor(value: RGBAColor = RGBAColor.BLACK) = networkedData(value, RGBACodec)

// networks enum as unsigned variable length integer
fun <E : Enum<E>> networkedEnum(value: E) = BasicNetworkedElement(value, StreamCodec.Enum(value::class.java))
fun <E : Enum<E>> networkedEnum(values: List<E>, value: E = values.first()) = BasicNetworkedElement(value, VarIntValueCodec.map({ values[this] }, { ordinal }))

// networks enum as a int32_t on legacy protocol
fun <E : Enum<E>> networkedEnumSortOfStupid(value: E): BasicNetworkedElement<E, Int> {
	val codec = StreamCodec.Enum(value::class.java)
	return BasicNetworkedElement(value, codec, IntValueCodec, { it.ordinal }, { codec.values[it] })
}

// networks enum as a int32_t on legacy protocol
fun <E : Enum<E>> networkedEnumSortOfStupid(values: List<E>, value: E = values.first()): BasicNetworkedElement<E, Int> {
	return BasicNetworkedElement(value, VarIntValueCodec.map({ values[this] }, { ordinal }), IntValueCodec, { it.ordinal }, { values[it] })
}

// networks enum as a signed variable length integer on legacy protocol
fun <E : Enum<E>> networkedEnumStupid(value: E): BasicNetworkedElement<E, Int> {
	val codec = StreamCodec.Enum(value::class.java)
	return BasicNetworkedElement(value, codec, VarIntValueCodec, { it.ordinal }, { codec.values[it] })
}

// networks enum as string on legacy protocol
fun <E : Enum<E>> networkedEnumExtraStupid(value: E): BasicNetworkedElement<E, String> {
	val codec = StreamCodec.Enum(value::class.java)
	return BasicNetworkedElement(value, codec, InternedStringCodec, { if (it is IStringSerializable) it.jsonName else it.name }, { s -> codec.values.firstOrNull { if (it is IStringSerializable) it.match(s) else it.name == s } ?: throw NoSuchElementException(s) })
}
