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

import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonParseException
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSyntaxException
import io.netty.buffer.ByteBufInputStream
import io.netty.buffer.ByteBufOutputStream
import io.netty.handler.codec.EncoderException
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.NbtAccounter
import net.minecraft.nbt.NbtIo
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.chat.Component
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.material.Fluid
import net.minecraftforge.fluids.FluidStack
import net.minecraftforge.registries.ForgeRegistries
import net.minecraftforge.registries.ForgeRegistry
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(accounter: NbtAccounter = NbtAccounter(1L shl 18 /* 256 KiB */)): CompoundTag {
	return try {
		NbtIo.read(if (this is DataInputStream) this else DataInputStream(this), accounter)
	} catch (ioexception: IOException) {
		throw EncoderException(ioexception)
	}
}

fun OutputStream.writeItem(itemStack: ItemStack, limitedTag: Boolean = true) {
	if (itemStack.isEmpty) {
		write(0)
	} else {
		write(1)
		val id = (ForgeRegistries.ITEMS as ForgeRegistry<Item>).getID(itemStack.item)

		writeInt(id)
		writeInt(itemStack.count)

		var compoundtag: CompoundTag? = null

		if (itemStack.item.isDamageable(itemStack) || itemStack.item.shouldOverrideMultiplayerNbt()) {
			compoundtag = if (limitedTag) itemStack.shareTag else itemStack.tag
		}

		write(if (compoundtag != null) 1 else 0)

		if (compoundtag != null) {
			writeNbt(compoundtag)
		}
	}
}

fun InputStream.readItem(sizeLimit: NbtAccounter = NbtAccounter(1L shl 18 /* 256 KiB */)): ItemStack {
	sizeLimit.accountBytes(1L)

	if (read() == 0) {
		return ItemStack.EMPTY
	}

	sizeLimit.accountBytes(9L)
	val item = (ForgeRegistries.ITEMS as ForgeRegistry<Item>).getValue(readInt())
	val itemStack = ItemStack(item, readInt())

	if (read() != 0) {
		itemStack.readShareTag(readNbt(sizeLimit))
	}

	return itemStack
}

fun OutputStream.writeFluidStack(value: FluidStack) {
	if (value.isEmpty) {
		write(0)
	} else {
		write(1)

		val id = (ForgeRegistries.FLUIDS as ForgeRegistry<Fluid>).getID(value.fluid)
		writeVarIntLE(id)
		writeInt(value.amount)

		if (value.hasTag()) {
			write(1)
			writeNbt(value.tag!!)
		} else {
			write(0)
		}
	}
}

fun InputStream.readFluidStack(sizeLimit: NbtAccounter = NbtAccounter(1L shl 14 /* 16 KiB */)): FluidStack {
	if (read() == 0) {
		return FluidStack.EMPTY
	} else {
		val id = readVarIntLE()
		val fluid = (ForgeRegistries.FLUIDS as ForgeRegistry<Fluid>).getValue(id) ?: return FluidStack.EMPTY
		val amount = readInt()

		if (read() > 0) {
			return FluidStack(fluid, amount, readNbt(sizeLimit))
		} else {
			return FluidStack(fluid, amount)
		}
	}
}

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

fun InputStream.readBigDecimal(sizeLimit: NbtAccounter = NbtAccounter(512L)): BigDecimal {
	val scale = readInt()
	val size = readVarIntLE(sizeLimit)
	require(size >= 0) { "Negative payload size: $size" }
	sizeLimit.accountBytes(size.toLong() + 4L)
	val bytes = ByteArray(size)
	read(bytes)
	return BigDecimal(BigInteger(bytes), scale)
}

private const val TYPE_NULL = 0x01
private const val TYPE_DOUBLE = 0x02
private const val TYPE_BOOLEAN = 0x03
private const val TYPE_INT = 0x04
private const val TYPE_STRING = 0x05
private const val TYPE_ARRAY = 0x06
private const val TYPE_OBJECT = 0x07

/**
 * Writes binary json to stream in Starbound Object Notation format
 *
 * just copy pasted this code from my another project because i was lazy
 */
fun OutputStream.writeJson(element: JsonElement) {
	if (element is JsonObject) {
		write(TYPE_OBJECT)
		writeVarIntLE(element.size())

		for ((k, v) in element.entrySet()) {
			writeBinaryString(k)
			writeJson(v)
		}
	} else if (element is JsonArray) {
		write(TYPE_ARRAY)
		writeVarIntLE(element.size())

		for (v in element) {
			writeJson(v)
		}
	} else if (element is JsonPrimitive) {
		if (element.isNumber) {
			val num = element.asNumber

			if (num is Int || num is Long || num is Short || num is Byte) {
				write(TYPE_INT)
				writeVarLongLE(num.toLong())
			} else if (num is Float || num is Double) {
				write(TYPE_DOUBLE)
				writeDouble(num.toDouble())
			} else {
				throw IllegalArgumentException("Unknown number type: ${num::class.qualifiedName}")
			}
		} else if (element.isString) {
			write(TYPE_STRING)
			writeBinaryString(element.asString)
		} else if (element.isBoolean) {
			write(TYPE_BOOLEAN)
			write(if (element.asBoolean) 1 else 0)
		} else {
			write(TYPE_NULL)
		}
	} else if (element is JsonNull) {
		write(TYPE_NULL)
	} else {
		throw IllegalArgumentException("Unknown element type: ${element::class.qualifiedName}")
	}
}

/**
 * Reads binary json from stream in Starbound Object Notation format
 *
 * just copy pasted this code from my another project because i was lazy
 */
fun InputStream.readJson(sizeLimit: NbtAccounter = NbtAccounter(1L shl 18 /* 256 KiB */)): JsonElement {
	sizeLimit.accountBytes(1L)

	return when (val id = read()) {
		TYPE_NULL -> JsonNull.INSTANCE
		TYPE_DOUBLE -> {
			sizeLimit.accountBytes(8L)
			JsonPrimitive(readDouble())
		}
		TYPE_BOOLEAN -> {
			sizeLimit.accountBytes(1L)
			JsonPrimitive(read() > 1)
		}
		TYPE_INT -> JsonPrimitive(readVarLongLE(sizeLimit))
		TYPE_STRING -> JsonPrimitive(readBinaryString(sizeLimit))
		TYPE_ARRAY -> {
			val values = readVarIntLE(sizeLimit)

			if (values == 0) return JsonArray()
			if (values < 0) throw JsonSyntaxException("Tried to read json array with $values elements in it")

			val build = JsonArray(values)
			for (i in 0 until values) build.add(readJson(sizeLimit))
			return build
		}
		TYPE_OBJECT -> {
			val values = readVarIntLE(sizeLimit)
			if (values == 0) return JsonObject()
			if (values < 0) throw JsonSyntaxException("Tried to read json object with $values elements in it")

			val build = JsonObject()

			for (i in 0 until values) {
				val key: String

				try {
					key = readBinaryString(sizeLimit)
				} catch(err: Throwable) {
					throw JsonSyntaxException("Reading json object at $i", err)
				}

				try {
					build.add(key, readJson(sizeLimit))
				} catch(err: Throwable) {
					throw JsonSyntaxException("Reading json object at $i with name $key", err)
				}
			}

			return build
		}
		else -> throw JsonParseException("Unknown element type $id")
	}
}

fun InputStream.readBinaryComponent(): Component? {
	return Component.Serializer.fromJson(readJson())
}

fun OutputStream.writeBinaryComponent(component: Component) {
	writeJson(Component.Serializer.toJsonTree(component))
}

fun FriendlyByteBuf.readJson(sizeLimit: NbtAccounter = NbtAccounter(1L shl 18)): JsonElement {
	return ByteBufInputStream(this).readJson(sizeLimit)
}

fun FriendlyByteBuf.writeJson(value: JsonElement) {
	ByteBufOutputStream(this).writeJson(value)
}

fun FriendlyByteBuf.readBinaryComponent(): Component {
	return Component.Serializer.fromJson(readJson()) ?: throw NullPointerException("Received null component")
}

fun FriendlyByteBuf.writeBinaryComponent(component: Component) {
	writeJson(Component.Serializer.toJsonTree(component))
}

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)
