package ru.dbotthepony.mc.otm.core.util

import io.netty.handler.codec.EncoderException
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtAccounter
import net.minecraft.nbt.NbtIo
import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.material.Fluid
import net.neoforged.neoforge.fluids.FluidStack
import java.io.*
import java.math.BigDecimal
import java.math.BigInteger
import kotlin.math.absoluteValue

// But seriously, Mojang, why would you need to derive from ByteBuf directly, when you can implement
// your own InputStream and OutputStream, since ByteBuf is meant to be operated on most time like a stream anyway?

// netty ByteBuf -> netty ByteBufInputStream -> Minecraft FriendlyInputStream

fun OutputStream.writeNbt(value: CompoundTag) {
	try {
		NbtIo.write(value, if (this is DataOutputStream) this else DataOutputStream(this))
	} catch (ioexception: IOException) {
		throw EncoderException(ioexception)
	}
}

fun InputStream.readNbt(): CompoundTag {
	return try {
		NbtIo.read(if (this is DataInputStream) this else DataInputStream(this))
	} catch (ioexception: IOException) {
		throw EncoderException(ioexception)
	}
}

fun OutputStream.writeBigDecimal(value: BigDecimal) {
	writeInt(value.scale())
	val bytes = value.unscaledValue().toByteArray()
	writeVarIntLE(bytes.size)
	write(bytes)
}

fun InputStream.readBigDecimal(): BigDecimal {
	val scale = readInt()
	val size = readVarIntLE()
	require(size >= 0) { "Negative payload size: $size" }
	val bytes = ByteArray(size)
	read(bytes)
	return BigDecimal(BigInteger(bytes), scale)
}

fun <S : OutputStream, V> S.writeCollection(collection: Collection<V>, writer: S.(V) -> Unit) {
	writeVarIntLE(collection.size)

	for (value in collection) {
		writer(this, value)
	}
}

fun <S : InputStream, V, C : MutableCollection<V>> S.readCollection(reader: S.() -> V, factory: (Int) -> C): C {
	val size = readVarIntLE()
	val collection = factory.invoke(size)

	for (i in 0 until size) {
		collection.add(reader(this))
	}

	return collection
}

fun <S : InputStream, V> S.readCollection(reader: S.() -> V) = readCollection(reader, ::ArrayList)

fun OutputStream.writeInt(value: Int) {
	if (this is DataOutput) {
		writeInt(value)
		return
	}

	write(value ushr 24)
	write(value ushr 16)
	write(value ushr 8)
	write(value)
}

fun InputStream.readInt(): Int {
	if (this is DataInput) {
		return readInt()
	}

	return (read() shl 24) or (read() shl 16) or (read() shl 8) or read()
}

fun OutputStream.writeLong(value: Long) {
	if (this is DataOutput) {
		writeLong(value)
		return
	}

	write((value ushr 48).toInt())
	write((value ushr 40).toInt())
	write((value ushr 32).toInt())
	write((value ushr 24).toInt())
	write((value ushr 16).toInt())
	write((value ushr 8).toInt())
	write(value.toInt())
}

fun InputStream.readLong(): Long {
	if (this is DataInput) {
		return readLong()
	}

	return (read().toLong() shl 48) or
		(read().toLong() shl 40) or
		(read().toLong() shl 32) or
		(read().toLong() shl 24) or
		(read().toLong() shl 16) or
		(read().toLong() shl 8) or
		read().toLong()
}

fun OutputStream.writeFloat(value: Float) = writeInt(value.toBits())
fun InputStream.readFloat() = Float.fromBits(readInt())
fun OutputStream.writeDouble(value: Double) = writeLong(value.toBits())
fun InputStream.readDouble() = Double.fromBits(readLong())

fun InputStream.readVarIntLE(): Int {
	val readFirst = read()

	if (readFirst < 0) {
		throw NoSuchElementException("Reached end of stream")
	}

	if (readFirst and 64 == 0) {
		return if (readFirst and 128 != 0) -(readFirst and 63) else readFirst and 63
	}

	var result = 0
	var nextBit = readFirst and 64
	var read = readFirst and 63
	var i = 0

	while (nextBit != 0) {
		result = result or (read shl i)
		read = read()

		if (read < 0) {
			throw NoSuchElementException("Reached end of stream")
		}

		nextBit = read and 128
		read = read and 127

		if (i == 0)
			i = 6
		else
			i += 7
	}

	result = result or (read shl i)
	return if (readFirst and 128 != 0) -result else result
}

fun OutputStream.writeVarIntLE(value: Int) {
	write((if (value < 0) 128 else 0) or (if (value in -63 .. 63) 0 else 64) or (value.absoluteValue and 63))
	var written = value.absoluteValue ushr 6

	while (written != 0) {
		write((written and 127) or (if (written >= 128) 128 else 0))
		written = written ushr 7
	}
}

fun InputStream.readVarLongLE(sizeLimit: NbtAccounter? = null): Long {
	sizeLimit?.accountBytes(1L)

	val readFirst = read()

	if (readFirst < 0) {
		throw NoSuchElementException("Reached end of stream")
	}

	if (readFirst and 64 == 0) {
		return if (readFirst and 128 != 0) -(readFirst and 63).toLong() else (readFirst and 63).toLong()
	}

	var result = 0L
	var nextBit = readFirst and 64
	var read = readFirst and 63
	var i = 0

	while (nextBit != 0) {
		result = result or (read shl i).toLong()
		sizeLimit?.accountBytes(1L)
		read = read()

		if (read < 0) {
			throw NoSuchElementException("Reached end of stream")
		}

		nextBit = read and 128
		read = read and 127

		if (i == 0)
			i = 6
		else
			i += 7
	}

	result = result or (read shl i).toLong()
	return if (readFirst and 128 != 0) -result else result
}

fun OutputStream.writeVarLongLE(value: Long) {
	write((if (value < 0L) 128 else 0) or (if (value in -63 .. 63) 0 else 64) or (value.absoluteValue and 63).toInt())
	var written = value.absoluteValue ushr 6

	while (written != 0L) {
		write((written and 127).toInt() or (if (written >= 128) 128 else 0))
		written = written ushr 7
	}
}

fun InputStream.readBinaryString(): String {
	val size = readVarIntLE()
	require(size >= 0) { "Negative payload size: $size" }
	val bytes = ByteArray(size)
	read(bytes)
	return bytes.decodeToString()
}

fun OutputStream.writeBinaryString(input: String) {
	val bytes = input.encodeToByteArray()
	writeVarIntLE(bytes.size)
	write(bytes)
}
