package ru.dbotthepony.mc.otm.network

import it.unimi.dsi.fastutil.bytes.ByteArrayList
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.chat.Component
import net.minecraft.network.codec.StreamCodec
import net.minecraft.network.protocol.common.custom.CustomPacketPayload
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundSource
import net.neoforged.neoforge.network.handling.IPayloadContext
import ru.dbotthepony.mc.otm.OverdriveThatMatters
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.capability.energy.extractEnergyExact
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.config.AndroidConfig
import ru.dbotthepony.mc.otm.core.ResourceLocation
import ru.dbotthepony.mc.otm.core.readComponent
import ru.dbotthepony.mc.otm.core.writeComponent
import ru.dbotthepony.mc.otm.menu.tech.AndroidStationMenu
import ru.dbotthepony.mc.otm.onceServer
import ru.dbotthepony.mc.otm.registry.game.AndroidFeatures
import ru.dbotthepony.mc.otm.registry.MRegistry
import ru.dbotthepony.mc.otm.registry.game.MSoundEvents

class AndroidFeatureSyncPacket(val type: AndroidFeatureType<*>, val data: ByteArrayList) : CustomPacketPayload {
	fun write(buff: RegistryFriendlyByteBuf) {
		buff.writeInt(MRegistry.ANDROID_FEATURES.getId(type))
		buff.writeBytes(data.elements(), 0, data.size)
	}

	fun play(context: IPayloadContext) {
		val android = minecraft.player?.matteryPlayer ?: return
		android.computeIfAbsent(type).syncher.read(context.player().registryAccess(), data)
	}

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

	companion object {
		val TYPE = CustomPacketPayload.Type<AndroidFeatureSyncPacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"android_feature_sync"
			)
		)

		val CODEC: StreamCodec<RegistryFriendlyByteBuf, AndroidFeatureSyncPacket> =
			StreamCodec.ofMember(AndroidFeatureSyncPacket::write, ::read)

		fun read(buff: RegistryFriendlyByteBuf): AndroidFeatureSyncPacket {
			return AndroidFeatureSyncPacket(
				MRegistry.ANDROID_FEATURES.byIdOrThrow(buff.readInt()),
				ByteArrayList.wrap(ByteArray(buff.readableBytes()).also { buff.readBytes(it) })
			)
		}
	}
}

class AndroidResearchSyncPacket(val type: AndroidResearchType, val dataList: ByteArrayList) :
	CustomPacketPayload {
	fun write(buff: FriendlyByteBuf) {
		buff.writeUtf(type.id.toString())
		buff.writeBytes(dataList.elements(), 0, dataList.size)
	}

	fun play(context: IPayloadContext) {
		val android = minecraft.player?.matteryPlayer ?: return
		android.getResearch(type).syncher.read(context.player().registryAccess(), dataList)
	}

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

	companion object {
		val TYPE = CustomPacketPayload.Type<AndroidResearchSyncPacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"android_research_sync"
			)
		)

		val CODEC: StreamCodec<FriendlyByteBuf, AndroidResearchSyncPacket> =
			StreamCodec.ofMember(AndroidResearchSyncPacket::write, ::read)

		fun read(buff: FriendlyByteBuf): AndroidResearchSyncPacket {
			return AndroidResearchSyncPacket(
				AndroidResearchManager[net.minecraft.resources.ResourceLocation.parse(buff.readUtf())] ?: throw NoSuchElementException(),
				ByteArrayList.wrap(ByteArray(buff.readableBytes()).also { buff.readBytes(it) })
			)
		}
	}
}

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

	fun play(context: IPayloadContext) {
		val ply = context.player()
		if (ply.isSpectator) return

		if (!ply.matteryPlayer.isAndroid || ply.containerMenu !is AndroidStationMenu)
			return

		ply.matteryPlayer.getResearch(type).research()
	}

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

	companion object {
		val TYPE = CustomPacketPayload.Type<AndroidResearchRequestPacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"android_research_request"
			)
		)

		val CODEC: StreamCodec<FriendlyByteBuf, AndroidResearchRequestPacket> = StreamCodec.ofMember(AndroidResearchRequestPacket::write, ::read)

		fun read(buff: FriendlyByteBuf): AndroidResearchRequestPacket {
			return AndroidResearchRequestPacket(
				AndroidResearchManager[net.minecraft.resources.ResourceLocation.parse(buff.readUtf())] ?: throw NoSuchElementException()
			)
		}
	}
}

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

	fun play(context: IPayloadContext) {
		val android = minecraft.player?.matteryPlayer ?: return
		android.removeFeature(type)
	}

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

	companion object {
		val TYPE = CustomPacketPayload.Type<AndroidFeatureRemovePacket>(
			ResourceLocation(
				OverdriveThatMatters.MOD_ID,
				"android_feature_remove"
			)
		)

		val CODEC: StreamCodec<FriendlyByteBuf, AndroidFeatureRemovePacket> =
			StreamCodec.ofMember(AndroidFeatureRemovePacket::write, ::read)

		fun read(buff: FriendlyByteBuf): AndroidFeatureRemovePacket {
			return AndroidFeatureRemovePacket(MRegistry.ANDROID_FEATURES.byIdOrThrow(buff.readInt()))
		}
	}
}

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

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

	fun play(context: IPayloadContext) {
		MatteryGUI.AndroidIterationLayer.iteration = iteration
		MatteryGUI.AndroidIterationLayer.deathLog.clear()
		MatteryGUI.AndroidIterationLayer.deathLog.addAll(deathLog)
		MatteryGUI.AndroidIterationLayer.showIterationUntil = System.currentTimeMillis() + 4000L
		MatteryGUI.AndroidIterationLayer.showIterationUntilFade = System.currentTimeMillis() + 5000L
	}

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

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

		fun read(buff: RegistryFriendlyByteBuf): 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 SwitchAndroidFeaturePacket(val type: AndroidFeatureType<*>, val newState: Boolean) : CustomPacketPayload {
	fun write(buff: FriendlyByteBuf) {
		buff.writeInt(MRegistry.ANDROID_FEATURES.getId(type))
		buff.writeBoolean(newState)
	}

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

		if (!matteryPlayer.isAndroid) {
			return
		}

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

		if (feature is AndroidActiveFeature && feature.allowToSwitchByPlayer && (!matteryPlayer.ply.isSpectator || feature.allowToSwitchByPlayerWhileSpectator)) {
			matteryPlayer.features
				.map { it as? AndroidActiveFeature }
				.filter { it != null && 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
		}
	}

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

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

		fun read(buff: FriendlyByteBuf): SwitchAndroidFeaturePacket {
			return SwitchAndroidFeaturePacket(MRegistry.ANDROID_FEATURES.byIdOrThrow(buff.readInt()), buff.readBoolean())
		}
	}
}

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

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

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

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

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

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

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

		fun read(buff: FriendlyByteBuf): ActivateAndroidFeaturePacket {
			return ActivateAndroidFeaturePacket(MRegistry.ANDROID_FEATURES.byIdOrThrow(buff.readInt()))
		}
	}
}

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

	fun play(context: IPayloadContext) {
		GlitchRenderer.glitchFor(millis)
	}

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

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

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

object TriggerShockwavePacket : CustomPacketPayload {
	fun play(context: IPayloadContext) {
		val shockwave = context.player().matteryPlayer.getFeature(AndroidFeatures.SHOCKWAVE) ?: return

		if (!shockwave.isOnCooldown && shockwave.isActive && shockwave.airTicks > 0) {
			onceServer { // delay by one tick so player update its position as well
				shockwave.shockwave()
			}
		}
	}

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

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

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

		if (!mattery.isAndroid)
			return

		val feature = mattery.getFeature(AndroidFeatures.JUMP_BOOST) ?: return

		if (feature.isActive && feature.cooldown <= 4 && mattery.androidEnergy.extractEnergyExact(AndroidConfig.JumpBoost.ENERGY_COST, false)) {
			feature.putOnCooldown()

			context.player().level().playSound(
				context.player(), context.player(),
				MSoundEvents.ANDROID_JUMP_BOOST, SoundSource.PLAYERS,
				1f, 1f
			)

			SmokeParticlesPacket.makeSmoke(context.player() as ServerPlayer, context.player().x, context.player().y, context.player().z)
		}
	}

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

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