package ru.dbotthepony.mc.otm.network

import it.unimi.dsi.fastutil.bytes.ByteArrayList
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.codec.StreamCodec
import net.minecraft.network.protocol.common.custom.CustomPacketPayload
import net.minecraft.network.protocol.game.ClientboundSetCarriedItemPacket
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.item.ItemStack
import net.neoforged.neoforge.network.handling.IPayloadContext
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.capability.MatteryPlayer
import ru.dbotthepony.mc.otm.capability.matteryPlayer
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.container.get
import ru.dbotthepony.mc.otm.container.set
import ru.dbotthepony.mc.otm.core.ResourceLocation
import ru.dbotthepony.mc.otm.core.math.component1
import ru.dbotthepony.mc.otm.core.math.component2
import ru.dbotthepony.mc.otm.core.math.component3
import ru.dbotthepony.mc.otm.core.math.toRadians
import ru.dbotthepony.mc.otm.core.otmRandom
import ru.dbotthepony.mc.otm.core.position
import ru.dbotthepony.mc.otm.core.readItem
import ru.dbotthepony.mc.otm.core.writeItem
import ru.dbotthepony.mc.otm.menu.ExopackInventoryMenu
import java.util.*

class MatteryPlayerDataPacket(val bytes: ByteArrayList, val isPublic: Boolean, val target: UUID? = null) : CustomPacketPayload {
	fun write(buff: FriendlyByteBuf) {
		buff.writeBoolean(target != null)

		if (target != null)
			buff.writeUUID(target)

		buff.writeBoolean(isPublic)
		buff.writeBytes(bytes.elements(), 0, bytes.size)
	}

	fun play(context: IPayloadContext) {
		val player: MatteryPlayer

		if (target != null) {
			player = minecraft.level?.players()?.firstOrNull { it.uuid == target }?.matteryPlayer ?: return
		} else {
			player = minecraft.player?.matteryPlayer ?: return
		}

		if (isPublic) {
			player.publicSyncher.read(context.player().registryAccess(), bytes)
		} else {
			player.syncher.read(context.player().registryAccess(), bytes)
		}
	}

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

	companion object {
		val TYPE = CustomPacketPayload.Type<MatteryPlayerDataPacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"player_data"
			)
		)
		val CODEC: StreamCodec<FriendlyByteBuf, MatteryPlayerDataPacket> =
			StreamCodec.ofMember(MatteryPlayerDataPacket::write, ::read)

		fun read(buff: FriendlyByteBuf): MatteryPlayerDataPacket {
			val target = if (buff.readBoolean())
				buff.readUUID()
			else
				null

			val isPublic = buff.readBoolean()
			val readable = buff.readableBytes()
			return MatteryPlayerDataPacket(ByteArrayList.wrap(ByteArray(readable).also(buff::readBytes)), isPublic, target)
		}
	}
}

class ExopackCarriedPacket(val itemStack: ItemStack, val containerState: Int) : CustomPacketPayload {
	fun write(buff: RegistryFriendlyByteBuf) {
		buff.writeItem(itemStack)
		buff.writeInt(containerState)
	}

	fun play(context: IPayloadContext) {
		val mattery = minecraft.player?.matteryPlayer ?: return

		if (mattery.hasExopack) {
			mattery.exoPackMenu.carried = itemStack
			mattery.exoPackMenu.stateId = containerState
		}
	}

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

	companion object {
		val TYPE = CustomPacketPayload.Type<ExopackCarriedPacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"exopack_carried"
			)
		)
		val CODEC: StreamCodec<RegistryFriendlyByteBuf, ExopackCarriedPacket> =
			StreamCodec.ofMember(ExopackCarriedPacket::write, ::read)

		fun read(buff: RegistryFriendlyByteBuf): ExopackCarriedPacket {
			return ExopackCarriedPacket(buff.readItem(), buff.readInt())
		}
	}
}

class ExopackSlotPacket(val slotId: Int, val itemStack: ItemStack, val containerState: Int) : CustomPacketPayload {
	fun write(buff: RegistryFriendlyByteBuf) {
		buff.writeInt(slotId)
		buff.writeItem(itemStack)
		buff.writeInt(containerState)
	}

	fun play(context: IPayloadContext) {
		if (slotId < 0) {
			LOGGER.error("Unknown slot with ID {} in exosuit menu", slotId)
			return
		}

		val mattery = minecraft.player?.matteryPlayer ?: return

		if (mattery.hasExopack) {
			if (slotId >= mattery.exoPackMenu.slots.size) {
				LOGGER.error("Unknown slot with ID {} in exosuit menu", slotId)
				return
			}

			// don't duplicate data
			// really.
			if (mattery.exoPackMenu.slots[slotId].container == minecraft.player?.inventory && minecraft.player?.containerMenu !is ExopackInventoryMenu)
				return

			mattery.exoPackMenu.slots[slotId].set(itemStack)
			mattery.exoPackMenu.stateId = containerState
		}
	}

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

	companion object {
		private val LOGGER = LogManager.getLogger()

		val TYPE = CustomPacketPayload.Type<ExopackSlotPacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"exopack_slot"
			)
		)
		val CODEC: StreamCodec<RegistryFriendlyByteBuf, ExopackSlotPacket> =
			StreamCodec.ofMember(ExopackSlotPacket::write, ::read)

		fun read(buff: RegistryFriendlyByteBuf): ExopackSlotPacket {
			return ExopackSlotPacket(buff.readInt(), buff.readItem(), buff.readInt())
		}
	}
}

class ExopackMenuInitPacket(val slots: List<ItemStack>, val carried: ItemStack, val containerState: Int) : CustomPacketPayload {
	fun write(buff: RegistryFriendlyByteBuf) {
		buff.writeInt(slots.size)

		for (item in slots) {
			buff.writeItem(item)
		}

		buff.writeItem(carried)
		buff.writeInt(containerState)
	}

	fun play(context: IPayloadContext) {
		val mattery = minecraft.player?.matteryPlayer ?: return

		if (mattery.hasExopack) {
			mattery.exoPackMenu.initializeContents(containerState, slots, carried)
		}
	}

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

	companion object {
		val TYPE = CustomPacketPayload.Type<ExopackMenuInitPacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"exopack_menu_init"
			)
		)
		val CODEC: StreamCodec<RegistryFriendlyByteBuf, ExopackMenuInitPacket> =
			StreamCodec.ofMember(ExopackMenuInitPacket::write, ::read)

		fun read(buff: RegistryFriendlyByteBuf): ExopackMenuInitPacket {
			val size = buff.readInt()
			val slots = ArrayList<ItemStack>(size)

			for (i in 0 until size) {
				slots.add(buff.readItem())
			}

			val carried = buff.readItem()
			val containerState = buff.readInt()

			return ExopackMenuInitPacket(slots, carried, containerState)
		}
	}
}

object ExopackMenuOpen : CustomPacketPayload {
	fun play(context: IPayloadContext) {
		val player = context.player()
		val mattery = player.matteryPlayer

		if (mattery.hasExopack) {
			player.containerMenu = mattery.exoPackMenu
			mattery.exoPackMenu.enderChestOpenState.value = false
			mattery.exoPackMenu.furnaceMenuOpenState.value = false
		}
	}

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

	val TYPE = CustomPacketPayload.Type<ExopackMenuOpen>(
		ResourceLocation(
			OverdriveThatMatters.MOD_ID,
			"exopack_menu_open"
		)
	)

	val CODEC: StreamCodec<FriendlyByteBuf, ExopackMenuOpen> =
		StreamCodec.ofMember({ _, _ -> }, { ExopackMenuOpen })
}

class PickItemFromInventoryPacket(
	val targetHotbarSlot: Int,
	val sourceExosuitSlot: Int
) : CustomPacketPayload {
	fun write(buff: FriendlyByteBuf) {
		buff.writeVarInt(targetHotbarSlot)
		buff.writeVarInt(sourceExosuitSlot)
	}

	fun play(context: IPayloadContext) {
		val player = context.player() as ServerPlayer
		val mattery = player.matteryPlayer

		if (!mattery.hasExopack || sourceExosuitSlot !in 0 until mattery.exopackContainer.containerSize) {
			return
		}

		if (!Inventory.isHotbarSlot(targetHotbarSlot)) {
			return
		}

		player.inventory.selected = targetHotbarSlot
		val existingItem = player.inventory[targetHotbarSlot].copy()
		val inventoryItem = mattery.exopackContainer[sourceExosuitSlot].copy()

		player.inventory[targetHotbarSlot] = if (inventoryItem.isEmpty) ItemStack.EMPTY else inventoryItem
		mattery.exopackContainer[sourceExosuitSlot] = if (existingItem.isEmpty) ItemStack.EMPTY else existingItem

		player.connection.send(ClientboundSetCarriedItemPacket(targetHotbarSlot))
	}

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

	companion object {
		val TYPE = CustomPacketPayload.Type<PickItemFromInventoryPacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"pick_item_from_inventory"
			)
		)
		val CODEC: StreamCodec<FriendlyByteBuf, PickItemFromInventoryPacket> =
			StreamCodec.ofMember(PickItemFromInventoryPacket::write, ::read)

		fun read(buff: FriendlyByteBuf): PickItemFromInventoryPacket {
			return PickItemFromInventoryPacket(buff.readVarInt(), buff.readVarInt())
		}
	}
}

class SetExopackColorPacket(val color: RGBAColor) : CustomPacketPayload {
	fun write(buff: FriendlyByteBuf) {
		buff.writeFloat(color.red)
		buff.writeFloat(color.green)
		buff.writeFloat(color.blue)
	}

	fun play(context: IPayloadContext) {
		context.player().matteryPlayer.exopackColor = color
	}

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

	companion object {
		val TYPE = CustomPacketPayload.Type<SetExopackColorPacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"set_exopack_color"
			)
		)
		val CODEC: StreamCodec<FriendlyByteBuf, SetExopackColorPacket> =
			StreamCodec.ofMember(SetExopackColorPacket::write, ::read)

		fun read(buff: FriendlyByteBuf): SetExopackColorPacket {
			return SetExopackColorPacket(RGBAColor(buff.readFloat(), buff.readFloat(), buff.readFloat()))
		}
	}
}

object DisplayExopackPacket : CustomPacketPayload {
	fun play(context: IPayloadContext) {
		context.player().matteryPlayer.isExopackVisible = true
	}

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

	val TYPE = CustomPacketPayload.Type<DisplayExopackPacket>(
		ResourceLocation(
			OverdriveThatMatters.MOD_ID,
			"display_exopack"
		)
	)
	val CODEC: StreamCodec<FriendlyByteBuf, DisplayExopackPacket> =
		StreamCodec.ofMember({ _, _ -> }, { DisplayExopackPacket })
}

object HideExopackPacket : CustomPacketPayload {
	fun play(context: IPayloadContext) {
		context.player().matteryPlayer.isExopackVisible = false
	}

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

	val TYPE = CustomPacketPayload.Type<HideExopackPacket>(
		ResourceLocation(
			OverdriveThatMatters.MOD_ID,
			"hide_exopack"
		)
	)
	val CODEC: StreamCodec<FriendlyByteBuf, HideExopackPacket> =
		StreamCodec.ofMember({ _, _ -> }, { HideExopackPacket })
}

object EnableExopackGlowPacket : CustomPacketPayload {
	fun play(context: IPayloadContext) {
		context.player().matteryPlayer.exopackGlows = true
	}

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

	val TYPE = CustomPacketPayload.Type<EnableExopackGlowPacket>(
		ResourceLocation(
			OverdriveThatMatters.MOD_ID,
			"enable_exopack_glow"
		)
	)
	val CODEC: StreamCodec<FriendlyByteBuf, EnableExopackGlowPacket> =
		StreamCodec.ofMember({ _, _ -> }, { EnableExopackGlowPacket })
}

object DisableExopackGlowPacket : CustomPacketPayload {
	fun play(context: IPayloadContext) {
		context.player().matteryPlayer.exopackGlows = false
	}

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

	val TYPE = CustomPacketPayload.Type<DisableExopackGlowPacket>(
		ResourceLocation(
			OverdriveThatMatters.MOD_ID,
			"disable_exopack_glow"
		)
	)
	val CODEC: StreamCodec<FriendlyByteBuf, DisableExopackGlowPacket> =
		StreamCodec.ofMember({ _, _ -> }, { DisableExopackGlowPacket })
}

object ResetExopackColorPacket : CustomPacketPayload {
	fun play(context: IPayloadContext) {
		context.player().matteryPlayer.exopackColor = null
	}

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

	val TYPE = CustomPacketPayload.Type<ResetExopackColorPacket>(
		ResourceLocation(
			OverdriveThatMatters.MOD_ID,
			"reset_exopack_color"
		)
	)
	val CODEC: StreamCodec<FriendlyByteBuf, ResetExopackColorPacket> =
		StreamCodec.ofMember({ _, _ -> }, { ResetExopackColorPacket })
}

class ExopackSmokePacket(val player: UUID) : CustomPacketPayload {
	fun write(buff: FriendlyByteBuf) {
		buff.writeUUID(player)
	}

	fun play(context: IPayloadContext) {
		// minecraft.player?.level()?.getPlayerByUUID(player)?.matteryPlayer?.spawnExopackSmoke = true

		minecraft.player?.level()?.getPlayerByUUID(player)?.let { ply ->
			if (ply != minecraft.player || minecraft.gameRenderer.mainCamera.isDetached) {
				var (x, y, z) = ply.position

				y += 1.5
				val deg = toRadians(ply.yBodyRot + 90f)

				x += kotlin.math.cos(deg) * -0.4
				z += kotlin.math.sin(deg) * -0.4

				val level = ply.level()
				val random = level.otmRandom

				for (i in 0 .. random.nextInt(2, 4))
					level.addParticle(
						ParticleTypes.SMOKE,
						x + random.nextDouble() * 0.4 - 0.2,
						y + random.nextDouble() * 0.4 - 0.2,
						z + random.nextDouble() * 0.4 - 0.2,
						random.nextGaussian() * 0.02,
						random.nextGaussian() * 0.02,
						random.nextGaussian() * 0.02)
			}
		}
	}

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

	companion object {
		val TYPE = CustomPacketPayload.Type<ExopackSmokePacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"exopack_smoke"
			)
		)
		val CODEC: StreamCodec<FriendlyByteBuf, ExopackSmokePacket> =
			StreamCodec.ofMember(ExopackSmokePacket::write, ::read)

		fun read(buff: FriendlyByteBuf): ExopackSmokePacket {
			return ExopackSmokePacket(buff.readUUID())
		}
	}
}
