package ru.dbotthepony.kstarbound.io

import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import ru.dbotthepony.kommons.io.DelegateSyncher
import ru.dbotthepony.kommons.io.StreamCodec
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.io.readDouble
import ru.dbotthepony.kommons.io.readFloat
import ru.dbotthepony.kommons.io.readInt
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.readSignedVarLong
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeByteArray
import ru.dbotthepony.kommons.io.writeDouble
import ru.dbotthepony.kommons.io.writeFloat
import ru.dbotthepony.kommons.io.writeInt
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.io.writeSignedVarLong
import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kommons.io.writeStruct2i
import ru.dbotthepony.kommons.io.writeStruct3d
import ru.dbotthepony.kommons.io.writeStruct3f
import ru.dbotthepony.kommons.io.writeStruct3i
import ru.dbotthepony.kommons.io.writeStruct4d
import ru.dbotthepony.kommons.io.writeStruct4f
import ru.dbotthepony.kommons.io.writeStruct4i
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.DelegateGetter
import ru.dbotthepony.kommons.util.DelegateSetter
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.util.ListenableDelegate
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.math.AABB
import ru.dbotthepony.kstarbound.math.vector.Vector2d
import ru.dbotthepony.kstarbound.math.vector.Vector2f
import ru.dbotthepony.kstarbound.math.vector.Vector2i
import ru.dbotthepony.kstarbound.math.vector.Vector3d
import ru.dbotthepony.kstarbound.math.vector.Vector3f
import ru.dbotthepony.kstarbound.math.vector.Vector3i
import ru.dbotthepony.kstarbound.math.vector.Vector4d
import ru.dbotthepony.kstarbound.math.vector.Vector4f
import ru.dbotthepony.kstarbound.math.vector.Vector4i
import ru.dbotthepony.kstarbound.world.ChunkPos
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.EOFException
import java.io.InputStream
import java.io.OutputStream

fun InputStream.readByteChar(): Char {
	return read().toChar()
}

fun InputStream.readHeader(header: String) {
	for ((i, char) in header.withIndex()) {
		val read = readByteChar()

		if (read != char) {
			throw IllegalArgumentException("Malformed header at byte $i: expected $char (${char.code}) got $read (${char.code})")
		}
	}
}

fun InputStream.readChunkPos(): ChunkPos {
	return ChunkPos(readSignedVarInt(), readSignedVarInt())
}

fun OutputStream.writeColor(color: RGBAColor) {
	writeFloat(color.red)
	writeFloat(color.green)
	writeFloat(color.blue)
	writeFloat(color.alpha)
}

fun InputStream.readColor(): RGBAColor {
	return RGBAColor(readFloat(), readFloat(), readFloat(), readFloat())
}

fun InputStream.readInternedString(): String = Starbound.STRINGS.intern(readBinaryString())

fun InputStream.readAABBLegacy(): AABB {
	val mins = readVector2f()
	val maxs = readVector2f()
	return AABB(mins.toDoubleVector(), maxs.toDoubleVector())
}

fun <S : InputStream, L, R> S.readEither(isLegacy: Boolean, left: S.() -> L, right: S.() -> R): Either<L, R> {
	var type = readUnsignedByte()

	if (isLegacy)
		type--

	return when (type) {
		0 -> Either.left(left(this))
		1 -> Either.right(right(this))
		else -> throw IllegalArgumentException("Unexpected either type $type")
	}
}

fun <S : OutputStream, L, R> S.writeEither(value: Either<L, R>, isLegacy: Boolean, left: S.(L) -> Unit, right: S.(R) -> Unit) {
	if (value.isLeft) {
		if (isLegacy) write(1) else write(0)
		left(value.left())
	} else {
		if (isLegacy) write(2) else write(1)
		right(value.right())
	}
}

fun InputStream.readAABBLegacyOptional(): KOptional<AABB> {
	val mins = readVector2f()
	val maxs = readVector2f()

	// what the fuck are you doing?!
	if (
		mins.x == Float.MAX_VALUE && mins.y == Float.MAX_VALUE &&
		maxs.x == -Float.MAX_VALUE && maxs.y == -Float.MAX_VALUE
	) {
		return KOptional()
	}

	return KOptional(AABB(mins.toDoubleVector(), maxs.toDoubleVector()))
}

fun InputStream.readAABB(): AABB {
	return AABB(readVector2d(), readVector2d())
}

fun InputStream.readAABB(isLegacy: Boolean): AABB {
	if (isLegacy)
		return readAABBLegacy()
	else
		return readAABB()
}

fun OutputStream.writeAABBLegacy(value: AABB) {
	writeStruct2d(value.mins, true)
	writeStruct2d(value.maxs, true)
}

fun OutputStream.writeAABBLegacyOptional(value: KOptional<AABB>) {
	value.ifPresent {
		writeStruct2f(it.mins.toFloatVector())
		writeStruct2f(it.maxs.toFloatVector())
	}.ifNotPresent {
		writeFloat(Float.MAX_VALUE)
		writeFloat(Float.MAX_VALUE)
		writeFloat(-Float.MAX_VALUE)
		writeFloat(-Float.MAX_VALUE)
	}
}

fun OutputStream.writeAABB(value: AABB) {
	writeStruct2d(value.mins)
	writeStruct2d(value.maxs)
}

fun OutputStream.writeAABB(value: AABB, isLegacy: Boolean) {
	writeStruct2d(value.mins, isLegacy)
	writeStruct2d(value.maxs, isLegacy)
}

private fun InputStream.readBoolean(): Boolean {
	val read = read()

	if (read == -1)
		throw EOFException("End of stream reached")

	return read != 0
}

private fun InputStream.readUnsignedByte(): Int {
	val read = read()

	if (read == -1)
		throw EOFException("End of stream reached")

	return read
}

fun <S : InputStream, T> S.readNullable(reader: S.() -> T): T? {
	if (readBoolean())
		return reader(this)
	else
		return null
}

fun <S : OutputStream, T> S.writeNullable(value: T?, writer: S.(T) -> Unit) {
	if (value == null)
		write(0)
	else {
		write(1)
		writer(value)
	}
}

fun <S : InputStream, L, R> S.readMVariant2(left: S.() -> L, right: S.() -> R): Either<L, R>? {
	return when (val type = readUnsignedByte()) {
		0 -> null
		1 -> Either.left(left())
		2 -> Either.right(right())
		else -> throw IllegalArgumentException("Unknown variant type $type")
	}
}

fun <S : OutputStream, L, R> S.writeMVariant2(value: Either<L, R>?, left: S.(L) -> Unit, right: S.(R) -> Unit) {
	write(if (value == null) 0 else if (value.isLeft) 1 else 2)
	value?.map({ left(it) }, { right(it) })
}

fun InputStream.readNullableString() = readNullable { readInternedString() }
fun InputStream.readNullableFloat() = readNullable { readFloat() }
fun InputStream.readNullableDouble() = readNullable { readDouble() }
fun InputStream.readNullableDouble(isLegacy: Boolean) = readNullable { if (isLegacy) readFloat().toDouble() else readDouble() }
fun InputStream.readDouble(isLegacy: Boolean) = if (isLegacy) readFloat().toDouble() else readDouble()
fun InputStream.readVector2d(isLegacy: Boolean) = if (isLegacy) readVector2f().toDoubleVector() else readVector2d()

fun OutputStream.writeNullableString(value: String?) = writeNullable(value) { writeBinaryString(it) }
fun OutputStream.writeNullableFloat(value: Float?) = writeNullable(value) { writeFloat(it) }
fun OutputStream.writeNullableDouble(value: Double?) = writeNullable(value) { writeDouble(it) }
fun OutputStream.writeNullableDouble(value: Double?, isLegacy: Boolean) = writeNullable(value) { if (isLegacy) writeFloat(it.toFloat()) else writeDouble(it) }
fun OutputStream.writeDouble(value: Double, isLegacy: Boolean) = if (isLegacy) writeFloat(value.toFloat()) else writeDouble(value)
fun OutputStream.writeStruct2d(value: IStruct2d, isLegacy: Boolean) = if (isLegacy) { writeFloat(value.component1().toFloat()); writeFloat(value.component2().toFloat()) } else { writeDouble(value.component1()); writeDouble(value.component2()) }

private inline fun <T, I> InputStream.read2(factory: (I, I) -> T, reader: (InputStream) -> I): T {
	return factory(reader(this), reader(this))
}

private inline fun <T, I> InputStream.read3(factory: (I, I, I) -> T, reader: (InputStream) -> I): T {
	return factory(reader(this), reader(this), reader(this))
}

private inline fun <T, I> InputStream.read4(factory: (I, I, I, I) -> T, reader: (InputStream) -> I): T {
	return factory(reader(this), reader(this), reader(this), reader(this))
}

fun InputStream.readVector2i() = read2(::Vector2i) { it.readInt() }
fun InputStream.readVector2d() = read2(::Vector2d) { it.readDouble() }
fun InputStream.readVector2f() = read2(::Vector2f) { it.readFloat() }

fun InputStream.readVector3i() = read3(::Vector3i) { it.readInt() }
fun InputStream.readVector3d() = read3(::Vector3d) { it.readDouble() }
fun InputStream.readVector3f() = read3(::Vector3f) { it.readFloat() }

fun InputStream.readVector4i() = read4(::Vector4i) { it.readInt() }
fun InputStream.readVector4d() = read4(::Vector4d) { it.readDouble() }
fun InputStream.readVector4f() = read4(::Vector4f) { it.readFloat() }

val Vector2iCodec = StreamCodec.Impl(DataInputStream::readVector2i, DataOutputStream::writeStruct2i)
val Vector2dCodec = StreamCodec.Impl(DataInputStream::readVector2d, DataOutputStream::writeStruct2d)
val Vector2fCodec = StreamCodec.Impl(DataInputStream::readVector2f, DataOutputStream::writeStruct2f)

val Vector3iCodec = StreamCodec.Impl(DataInputStream::readVector3i, DataOutputStream::writeStruct3i)
val Vector3dCodec = StreamCodec.Impl(DataInputStream::readVector3d, DataOutputStream::writeStruct3d)
val Vector3fCodec = StreamCodec.Impl(DataInputStream::readVector3f, DataOutputStream::writeStruct3f)

val Vector4iCodec = StreamCodec.Impl(DataInputStream::readVector4i, DataOutputStream::writeStruct4i)
val Vector4dCodec = StreamCodec.Impl(DataInputStream::readVector4d, DataOutputStream::writeStruct4d)
val Vector4fCodec = StreamCodec.Impl(DataInputStream::readVector4f, DataOutputStream::writeStruct4f)

fun DelegateSyncher.vec2i(value: Vector2i, setter: DelegateSetter<Vector2i> = DelegateSetter.passthrough(), getter: DelegateGetter<Vector2i> = DelegateGetter.passthrough()) = Slot(
	ListenableDelegate.maskSmart(value, getter, setter), Vector2iCodec)
fun DelegateSyncher.vec2d(value: Vector2d, setter: DelegateSetter<Vector2d> = DelegateSetter.passthrough(), getter: DelegateGetter<Vector2d> = DelegateGetter.passthrough()) = Slot(
	ListenableDelegate.maskSmart(value, getter, setter), Vector2dCodec)
fun DelegateSyncher.vec2f(value: Vector2f, setter: DelegateSetter<Vector2f> = DelegateSetter.passthrough(), getter: DelegateGetter<Vector2f> = DelegateGetter.passthrough()) = Slot(
	ListenableDelegate.maskSmart(value, getter, setter), Vector2fCodec)

fun DelegateSyncher.vec3i(value: Vector3i, setter: DelegateSetter<Vector3i> = DelegateSetter.passthrough(), getter: DelegateGetter<Vector3i> = DelegateGetter.passthrough()) = Slot(
	ListenableDelegate.maskSmart(value, getter, setter), Vector3iCodec)
fun DelegateSyncher.vec3d(value: Vector3d, setter: DelegateSetter<Vector3d> = DelegateSetter.passthrough(), getter: DelegateGetter<Vector3d> = DelegateGetter.passthrough()) = Slot(
	ListenableDelegate.maskSmart(value, getter, setter), Vector3dCodec)
fun DelegateSyncher.vec3f(value: Vector3f, setter: DelegateSetter<Vector3f> = DelegateSetter.passthrough(), getter: DelegateGetter<Vector3f> = DelegateGetter.passthrough()) = Slot(
	ListenableDelegate.maskSmart(value, getter, setter), Vector3fCodec)

fun DelegateSyncher.vec4i(value: Vector4i, setter: DelegateSetter<Vector4i> = DelegateSetter.passthrough(), getter: DelegateGetter<Vector4i> = DelegateGetter.passthrough()) = Slot(
	ListenableDelegate.maskSmart(value, getter, setter), Vector4iCodec)
fun DelegateSyncher.vec4d(value: Vector4d, setter: DelegateSetter<Vector4d> = DelegateSetter.passthrough(), getter: DelegateGetter<Vector4d> = DelegateGetter.passthrough()) = Slot(
	ListenableDelegate.maskSmart(value, getter, setter), Vector4dCodec)
fun DelegateSyncher.vec4f(value: Vector4f, setter: DelegateSetter<Vector4f> = DelegateSetter.passthrough(), getter: DelegateGetter<Vector4f> = DelegateGetter.passthrough()) = Slot(
	ListenableDelegate.maskSmart(value, getter, setter), Vector4fCodec)

fun OutputStream.writeEnumStupid(index: Int, isLegacy: Boolean) {
	if (isLegacy) writeInt(index) else write(index)
}

fun InputStream.readEnumStupid(isLegacy: Boolean): Int {
	return if (isLegacy) readInt() else readUnsignedByte()
}

fun OutputStream.writeIntStupid(index: Int, isLegacy: Boolean) {
	if (isLegacy) writeInt(index) else writeSignedVarInt(index)
}

fun InputStream.readIntStupid(isLegacy: Boolean): Int {
	return if (isLegacy) readInt() else readSignedVarInt()
}

fun OutputStream.writeByteArray(array: ByteArrayList) {
	writeByteArray(array.elements(), 0, array.size)
}

fun OutputStream.writeByteArray(array: FastByteArrayOutputStream) {
	writeByteArray(array.array, 0, array.length)
}

fun OutputStream.writeDouble(value: Double, precision: Double, isLegacy: Boolean) {
	if (isLegacy) {
		writeSignedVarLong((value / precision).toLong())
	} else {
		writeDouble(value)
	}
}

fun InputStream.readDouble(precision: Double, isLegacy: Boolean): Double {
	if (isLegacy) {
		return readSignedVarLong() * precision
	} else {
		return readDouble()
	}
}
