package ru.dbotthepony.mc.otm.network

import it.unimi.dsi.fastutil.bytes.ByteArrayList
import it.unimi.dsi.fastutil.io.FastByteArrayInputStream
import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import net.minecraft.core.BlockPos
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.codec.StreamCodec
import net.minecraft.network.protocol.common.custom.CustomPacketPayload
import net.minecraft.world.level.Level
import net.neoforged.neoforge.network.handling.IPayloadContext
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.block.entity.MatteryBlockEntity
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.client.onceClient
import ru.dbotthepony.mc.otm.core.ResourceLocation
import ru.dbotthepony.mc.otm.core.collect.map
import ru.dbotthepony.mc.otm.core.collect.reduce
import java.util.*
import kotlin.collections.ArrayList

class BlockEntitySyncPacket(val position: BlockPos, val buffer: ByteArrayList) : CustomPacketPayload {
	fun write(buff: FriendlyByteBuf) {
		buff.writeBlockPos(position)
		buff.writeByteListUnbounded(buffer)
	}

	fun play(context: IPayloadContext) {
		val level = minecraft.player?.level()

		if (level == null) {
			LOGGER.error("Received BlockEntitySyncPacket before we are in valid level.")

			onceClient {
				play(context)
			}

			return
		}

		val blockEntity = level.getBlockEntity(position)

		if (blockEntity == null) {
			val list = backlog.computeIfAbsent(level) { Object2ObjectOpenHashMap() }
				.computeIfAbsent(position, Object2ObjectFunction { ArrayList() })

			list.add(buffer)
			LOGGER.warn("Putting BlockEntitySyncPacket received for $position into backlog (size: ${list.iterator().map { it.size }.reduce(0, Int::plus)} bytes) because there is literally no block entity there! This might cause memory leak.")
			return
		} else if (blockEntity !is MatteryBlockEntity) {
			LOGGER.warn("Dropping BlockEntitySyncPacket received for $position, because there is $blockEntity which is not MatteryBlockEntity!")
			backlog[level]?.remove(position)
			return
		}

		val packets = backlog[level]?.remove(position)

		try {
			if (packets != null) {
				LOGGER.info("Unfolding ${packets.size} backlog packets for $position (${level.getBlockState(position)}/$blockEntity)")

				for (packet in packets) {
					blockEntity.syncher.read(context.player().registryAccess(), packet)
				}
			}

			blockEntity.syncher.read(context.player().registryAccess(), buffer)
		} catch(err: Throwable) {
			LOGGER.error("Exception while reading synchronized BlockEntity data!\nPosition: $position\nBlock: ${level.getBlockState(position)}\nBlock entity: $blockEntity", err)
		}
	}

	override fun type(): CustomPacketPayload.Type<out CustomPacketPayload> {
		return TYPE
	}

	companion object {
		private val backlog = WeakHashMap<Level, Object2ObjectOpenHashMap<BlockPos, ArrayList<ByteArrayList>>>()

		val CODEC: StreamCodec<FriendlyByteBuf, BlockEntitySyncPacket> = StreamCodec.ofMember(BlockEntitySyncPacket::write, ::read)
		val TYPE = CustomPacketPayload.Type<BlockEntitySyncPacket>(ResourceLocation(OverdriveThatMatters.MOD_ID, "block_entity_data"))

		fun read(buff: FriendlyByteBuf): BlockEntitySyncPacket {
			return BlockEntitySyncPacket(buff.readBlockPos(), buff.readByteListUnbounded())
		}

		private val LOGGER = LogManager.getLogger()

		internal fun applyBacklog(blockEntity: MatteryBlockEntity) {
			val packets = backlog[blockEntity.level]?.remove(blockEntity.blockPos)

			try {
				if (packets != null) {
					LOGGER.info("Unfolding ${packets.size} backlog packets (size: ${packets.iterator().map { it.size }.reduce(0, Int::plus)} bytes) for ${blockEntity.blockPos} (${blockEntity.level!!.getBlockState(blockEntity.blockPos)}/$blockEntity)")

					for (packet in packets) {
						blockEntity.syncher.read(blockEntity.level!!.registryAccess(), packet)
					}
				}
			} catch(err: Throwable) {
				LOGGER.error("Exception while reading synchronized BlockEntity data!\nPosition: ${blockEntity.blockPos}\nBlock: ${blockEntity.level!!.getBlockState(blockEntity.blockPos)}\nBlock entity: $blockEntity", err)
			}
		}
	}
}
