package ru.dbotthepony.mc.otm.network

import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.chat.Component
import net.minecraft.network.protocol.game.ClientboundSetCarriedItemPacket
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraftforge.network.NetworkDirection.PLAY_TO_CLIENT
import net.minecraftforge.network.NetworkDirection.PLAY_TO_SERVER
import net.minecraftforge.network.NetworkEvent
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.android.AndroidActiveFeature
import ru.dbotthepony.mc.otm.android.AndroidFeatureType
import ru.dbotthepony.mc.otm.android.AndroidResearchManager
import ru.dbotthepony.mc.otm.android.AndroidResearchType
import ru.dbotthepony.mc.otm.android.AndroidSwitchableFeature
import ru.dbotthepony.mc.otm.android.feature.JumpBoostParticlesPacket
import ru.dbotthepony.mc.otm.android.feature.TriggerJumpBoostPacket
import ru.dbotthepony.mc.otm.android.feature.TriggerShockwavePacket
import ru.dbotthepony.mc.otm.capability.MatteryPlayerCapability
import ru.dbotthepony.mc.otm.capability.matteryPlayer
import ru.dbotthepony.mc.otm.client.MatteryGUI
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.client.render.GlitchRenderer
import ru.dbotthepony.mc.otm.client.render.ShockwaveRenderer
import ru.dbotthepony.mc.otm.container.get
import ru.dbotthepony.mc.otm.container.set
import ru.dbotthepony.mc.otm.core.math.RGBAColor
import ru.dbotthepony.mc.otm.core.math.Vector
import ru.dbotthepony.mc.otm.core.readItemType
import ru.dbotthepony.mc.otm.core.writeItemType
import ru.dbotthepony.mc.otm.menu.tech.AndroidStationMenu
import ru.dbotthepony.mc.otm.menu.ExoPackInventoryMenu
import ru.dbotthepony.mc.otm.registry.AndroidFeatures
import ru.dbotthepony.mc.otm.registry.MRegistry
import java.io.ByteArrayInputStream
import java.util.UUID
import java.util.function.Supplier

class MatteryPlayerFieldPacket(val bytes: ByteArray, val length: Int, val isPublic: Boolean, val target: UUID? = null) : MatteryPacket {
	constructor(stream: FastByteArrayOutputStream, isPublic: Boolean, target: UUID? = null) : this(stream.array, stream.length, isPublic, target)

	override fun write(buff: FriendlyByteBuf) {
		buff.writeBoolean(target != null)

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

		buff.writeBoolean(isPublic)
		buff.writeBytes(bytes, 0, length)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.get().enqueueWork {
			val player: MatteryPlayerCapability

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

			if (isPublic) {
				player.publicSynchronizer.read(ByteArrayInputStream(bytes, 0, length))
			} else {
				player.synchronizer.read(ByteArrayInputStream(bytes, 0, length))
			}
		}
	}

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

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

class AndroidResearchRequestPacket(val type: AndroidResearchType) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeUtf(type.id.toString())
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.enqueueWork {
			val ply = context.get().sender ?: return@enqueueWork
			if (ply.isSpectator) return@enqueueWork
			val android = ply.matteryPlayer ?: return@enqueueWork

			if (!android.isAndroid || ply.containerMenu !is AndroidStationMenu)
				return@enqueueWork

			android.getResearch(type).research()
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf): AndroidResearchRequestPacket {
			return AndroidResearchRequestPacket(AndroidResearchManager[ResourceLocation(buff.readUtf())] ?: throw NoSuchElementException())
		}
	}
}

class AndroidResearchSyncPacket(val type: AndroidResearchType, val dataList: FastByteArrayOutputStream?, val dataBytes: ByteArray?) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		dataList ?: throw NullPointerException("No byte list is present")
		buff.writeUtf(type.id.toString())
		buff.writeBytes(dataList.array, 0, dataList.length)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		dataBytes ?: throw NullPointerException("No data bytes array is present")

		context.get().packetHandled = true
		context.get().enqueueWork {
			val android = minecraft.player?.matteryPlayer ?: return@enqueueWork

			android.getResearch(type).applyNetworkPayload(ByteArrayInputStream(dataBytes))
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf): AndroidResearchSyncPacket {
			return AndroidResearchSyncPacket(
				AndroidResearchManager[ResourceLocation(buff.readUtf())] ?: throw NoSuchElementException(),
				null, ByteArray(buff.readableBytes()).also { buff.readBytes(it) }
			)
		}
	}
}

class AndroidFeatureSyncPacket(val type: AndroidFeatureType<*>, val dataList: FastByteArrayOutputStream?, val dataBytes: ByteArray?) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		dataList ?: throw NullPointerException("No byte list is present")
		buff.writeInt(MRegistry.ANDROID_FEATURES.getID(type))
		buff.writeBytes(dataList.array, 0, dataList.length)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		dataBytes ?: throw NullPointerException("No data bytes array is present")

		context.get().packetHandled = true
		context.get().enqueueWork {
			val android = minecraft.player?.matteryPlayer ?: return@enqueueWork

			android.computeIfAbsent(type).applyNetworkPayload(ByteArrayInputStream(dataBytes))
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf): AndroidFeatureSyncPacket {
			return AndroidFeatureSyncPacket(
				MRegistry.ANDROID_FEATURES.getValue(buff.readInt()),
				null, ByteArray(buff.readableBytes()).also { buff.readBytes(it) }
			)
		}
	}
}

class AndroidFeatureRemovePacket(val type: AndroidFeatureType<*>) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeInt(MRegistry.ANDROID_FEATURES.getID(type))
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.get().packetHandled = true
		context.get().enqueueWork {
			val android = minecraft.player?.matteryPlayer ?: return@enqueueWork

			android.removeFeature(type)
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf): AndroidFeatureRemovePacket {
			return AndroidFeatureRemovePacket(MRegistry.ANDROID_FEATURES.getValue(buff.readInt()))
		}
	}
}

class PlayerIterationPacket(val iteration: Int, val deathLog: List<Pair<Int, Component>>) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeInt(iteration)
		buff.writeInt(deathLog.size)

		for ((ticks, value) in deathLog) {
			buff.writeInt(ticks)
			buff.writeComponent(value)
		}
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true

		MatteryGUI.iteration = iteration
		MatteryGUI.deathLog.clear()
		MatteryGUI.deathLog.addAll(deathLog)
		MatteryGUI.showIterationUntil = System.currentTimeMillis() + 4000L
		MatteryGUI.showIterationUntilFade = System.currentTimeMillis() + 5000L
	}

	companion object {
		fun read(buff: FriendlyByteBuf): PlayerIterationPacket {
			val iteration = buff.readInt()
			val size = buff.readInt()

			val list = ArrayList<Pair<Int, Component>>()

			for (i in 0 until size) {
				list.add(buff.readInt() to buff.readComponent())
			}

			return PlayerIterationPacket(iteration, list)
		}
	}
}

class ExoSuitCarriedPacket(val itemStack: ItemStack, val containerState: Int) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeItem(itemStack)
		buff.writeInt(containerState)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.enqueueWork {
			val mattery = minecraft.player?.matteryPlayer ?: return@enqueueWork

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

	companion object {
		fun read(buff: FriendlyByteBuf): ExoSuitCarriedPacket {
			return ExoSuitCarriedPacket(buff.readItem(), buff.readInt())
		}
	}
}

class ExoSuitSlotPacket(val slotId: Int, val itemStack: ItemStack, val containerState: Int) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeInt(slotId)
		buff.writeItem(itemStack)
		buff.writeInt(containerState)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true

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

		context.enqueueWork {
			val mattery = minecraft.player?.matteryPlayer ?: return@enqueueWork

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

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

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

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

		fun read(buff: FriendlyByteBuf): ExoSuitSlotPacket {
			return ExoSuitSlotPacket(buff.readInt(), buff.readItem(), buff.readInt())
		}
	}
}

class ExoSuitMenuInitPacket(val slots: List<ItemStack>, val carried: ItemStack, val containerState: Int) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeInt(slots.size)

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

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

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.enqueueWork {
			val mattery = minecraft.player?.matteryPlayer ?: return@enqueueWork

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

	companion object {
		fun read(buff: FriendlyByteBuf): ExoSuitMenuInitPacket {
			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 ExoSuitMenuInitPacket(slots, carried, containerState)
		}
	}
}

object ExoPackMenuOpen : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.enqueueWork {
			val player = context.sender ?: return@enqueueWork
			val mattery = player.matteryPlayer ?: return@enqueueWork

			if (mattery.hasExoPack) {
				player.containerMenu = mattery.exoPackMenu
			}
		}
	}
}

class SwitchAndroidFeaturePacket(val type: AndroidFeatureType<*>, val newState: Boolean) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeInt(MRegistry.ANDROID_FEATURES.getID(type))
		buff.writeBoolean(newState)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true

		context.enqueueWork {
			val matteryPlayer = context.sender?.matteryPlayer ?: return@enqueueWork

			if (!matteryPlayer.isAndroid) {
				return@enqueueWork
			}

			val feature = matteryPlayer.getFeature(type) ?: return@enqueueWork

			if (feature is AndroidActiveFeature && feature.allowToSwitchByPlayer && (!matteryPlayer.ply.isSpectator || feature.allowToSwitchByPlayerWhileSpectator)) {
				matteryPlayer.features
					.map { it as? AndroidActiveFeature }
					.filter { it != null }
					.filter { it !== feature && it!!.allowToSwitchByPlayer && (!matteryPlayer.ply.isSpectator || it.allowToSwitchByPlayerWhileSpectator) }
					.forEach { it!!.isActive = false }
				feature.isActive = newState
			} else if (feature is AndroidSwitchableFeature && feature.allowToSwitchByPlayer && (!matteryPlayer.ply.isSpectator || feature.allowToSwitchByPlayerWhileSpectator)) {
				feature.isActive = newState
			}
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf): SwitchAndroidFeaturePacket {
			return SwitchAndroidFeaturePacket(MRegistry.ANDROID_FEATURES.getValue(buff.readInt()) ?: AndroidFeatures.AIR_BAGS, buff.readBoolean())
		}
	}
}

class ActivateAndroidFeaturePacket(val type: AndroidFeatureType<*>) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeInt(MRegistry.ANDROID_FEATURES.getID(type))
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true

		context.enqueueWork {
			val matteryPlayer = context.sender?.matteryPlayer ?: return@enqueueWork

			if (!matteryPlayer.isAndroid || matteryPlayer.ply.isSpectator) {
				return@enqueueWork
			}

			val feature = matteryPlayer.getFeature(type) as? AndroidActiveFeature ?: return@enqueueWork

			if (feature.isActive || feature.allowToSwitchByPlayer) {
				feature.activate(false)
			}
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf): ActivateAndroidFeaturePacket {
			return ActivateAndroidFeaturePacket(MRegistry.ANDROID_FEATURES.getValue(buff.readInt()) ?: AndroidFeatures.AIR_BAGS)
		}
	}
}

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

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true

		context.enqueueWork {
			val player = context.sender ?: return@enqueueWork
			val mattery = player.matteryPlayer ?: return@enqueueWork

			if (!mattery.hasExoPack || sourceExosuitSlot !in 0 until mattery.exoPackContainer.containerSize) {
				return@enqueueWork
			}

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

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

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

class GlitchPacket(val millis: Long) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeVarLong(millis)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		GlitchRenderer.glitchFor(millis)
	}

	companion object {
		fun read(buff: FriendlyByteBuf): GlitchPacket {
			return GlitchPacket(buff.readVarLong())
		}
	}
}

class ShockwaveEffectPacket(val pos: Vector) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeDouble(pos.x)
		buff.writeDouble(pos.y)
		buff.writeDouble(pos.z)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		ShockwaveRenderer.handle(this)
	}

	companion object {
		fun read(buff: FriendlyByteBuf): ShockwaveEffectPacket {
			return ShockwaveEffectPacket(Vector(buff.readDouble(), buff.readDouble(), buff.readDouble()))
		}
	}
}

object DisplayExopackPacket : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.sender?.matteryPlayer?.displayExoPack = true
	}
}

object HideExopackPacket : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.sender?.matteryPlayer?.displayExoPack = false
	}
}

object EnableExopackGlowPacket : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.sender?.matteryPlayer?.exopackGlows = true
	}
}

object DisableExopackGlowPacket : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.sender?.matteryPlayer?.exopackGlows = false
	}
}

object ResetExopackColorPacket : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.sender?.matteryPlayer?.exopackColor = null
	}
}

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

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.sender?.matteryPlayer?.exopackColor = color
	}

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

class SetInventoryFilterPacket(val type: Type, val slot: Int, val item: Item?) : MatteryPacket {
	enum class Type {
		INVENTORY,
		EXOPACK
	}

	override fun write(buff: FriendlyByteBuf) {
		buff.writeEnum(type)
		buff.writeVarInt(slot)

		if (item == null) {
			buff.writeByte(0)
		} else {
			buff.writeByte(1)
			buff.writeItemType(item)
		}
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		val player = context.sender?.matteryPlayer ?: return

		when (type) {
			Type.INVENTORY -> {
				if (slot in 0 until player.regularSlotFilters.size) {
					player.regularSlotFilters[slot].value = item
				}
			}

			Type.EXOPACK -> {
				if (slot in 0 until player.exoPackContainer.containerSize) {
					player.exoPackContainer.setSlotFilter(slot, item)
				}
			}
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf): SetInventoryFilterPacket {
			return SetInventoryFilterPacket(buff.readEnum(Type::class.java), buff.readVarInt(), if (buff.readByte() > 0) buff.readItemType() else null)
		}
	}
}

object MatteryPlayerNetworkChannel : MatteryNetworkChannel(
	version = "4",
	name = "player"
) {
	fun register() {
		add(MatteryPlayerFieldPacket::class, MatteryPlayerFieldPacket.Companion::read, PLAY_TO_CLIENT)

		add(AndroidResearchRequestPacket::class, AndroidResearchRequestPacket.Companion::read, PLAY_TO_SERVER)
		add(AndroidResearchSyncPacket::class, AndroidResearchSyncPacket.Companion::read, PLAY_TO_CLIENT)
		add(AndroidFeatureSyncPacket::class, AndroidFeatureSyncPacket.Companion::read, PLAY_TO_CLIENT)
		add(AndroidFeatureRemovePacket::class, AndroidFeatureRemovePacket.Companion::read, PLAY_TO_CLIENT)

		add(PlayerIterationPacket::class, PlayerIterationPacket.Companion::read, PLAY_TO_CLIENT)

		add(ExoSuitCarriedPacket::class, ExoSuitCarriedPacket.Companion::read, PLAY_TO_CLIENT)
		add(ExoSuitSlotPacket::class, ExoSuitSlotPacket.Companion::read, PLAY_TO_CLIENT)
		add(ExoSuitMenuInitPacket::class, ExoSuitMenuInitPacket.Companion::read, PLAY_TO_CLIENT)

		add(ExoPackMenuOpen::class, { ExoPackMenuOpen }, PLAY_TO_SERVER)

		add(SwitchAndroidFeaturePacket::class, SwitchAndroidFeaturePacket.Companion::read, PLAY_TO_SERVER)
		add(ActivateAndroidFeaturePacket::class, ActivateAndroidFeaturePacket.Companion::read, PLAY_TO_SERVER)

		add(TriggerShockwavePacket::class, { TriggerShockwavePacket }, PLAY_TO_SERVER)
		add(TriggerJumpBoostPacket::class, { TriggerJumpBoostPacket }, PLAY_TO_SERVER)
		add(JumpBoostParticlesPacket::class, JumpBoostParticlesPacket.Companion::read, PLAY_TO_CLIENT)

		add(PickItemFromInventoryPacket::class, PickItemFromInventoryPacket.Companion::read, PLAY_TO_SERVER)

		add(GlitchPacket::class, GlitchPacket.Companion::read, PLAY_TO_CLIENT)
		add(ShockwaveEffectPacket::class, ShockwaveEffectPacket.Companion::read, PLAY_TO_CLIENT)

		add(DisplayExopackPacket::class, { DisplayExopackPacket }, PLAY_TO_SERVER)
		add(HideExopackPacket::class, { HideExopackPacket }, PLAY_TO_SERVER)
		add(EnableExopackGlowPacket::class, { EnableExopackGlowPacket }, PLAY_TO_SERVER)
		add(DisableExopackGlowPacket::class, { DisableExopackGlowPacket }, PLAY_TO_SERVER)
		add(ResetExopackColorPacket::class, { ResetExopackColorPacket }, PLAY_TO_SERVER)
		add(SetExopackColorPacket::class, SetExopackColorPacket.Companion::read, PLAY_TO_SERVER)

		add(SetInventoryFilterPacket::class, SetInventoryFilterPacket.Companion::read, PLAY_TO_SERVER)
	}
}
