package ru.dbotthepony.kstarbound.io

import it.unimi.dsi.fastutil.bytes.ByteArrayList
import ru.dbotthepony.kommons.io.readBinaryString
import ru.dbotthepony.kommons.util.IStruct2d
import ru.dbotthepony.kommons.util.IStruct2i
import ru.dbotthepony.kommons.io.readDouble
import ru.dbotthepony.kommons.io.readFloat
import ru.dbotthepony.kommons.io.readLong
import ru.dbotthepony.kommons.io.readSignedVarInt
import ru.dbotthepony.kommons.io.readVector2d
import ru.dbotthepony.kommons.io.readVector2f
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeDouble
import ru.dbotthepony.kommons.io.writeFloat
import ru.dbotthepony.kommons.io.writeLong
import ru.dbotthepony.kommons.io.writeSignedVarInt
import ru.dbotthepony.kommons.io.writeStruct2d
import ru.dbotthepony.kommons.io.writeStruct2f
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.kommons.util.AABB
import ru.dbotthepony.kommons.util.Either
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.world.ChunkPos
import java.io.DataInput
import java.io.DataOutput
import java.io.EOFException
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.io.RandomAccessFile
import java.util.*
import java.util.function.IntConsumer
import java.util.function.IntSupplier
import kotlin.collections.ArrayList

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 OutputStream.writeAABBLegacy(value: AABB) {
	writeStruct2f(value.mins.toFloatVector())
	writeStruct2f(value.maxs.toFloatVector())
}

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)
}

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()) }
