package ru.dbotthepony.mc.otm.android.feature

import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.PoseStack
import it.unimi.dsi.fastutil.objects.ReferenceArraySet
import net.minecraft.ChatFormatting
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundSource
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.monster.warden.Warden
import net.minecraftforge.network.NetworkEvent
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.config.AndroidConfig
import ru.dbotthepony.mc.otm.android.AndroidResearchManager
import ru.dbotthepony.mc.otm.android.AndroidSwitchableFeature
import ru.dbotthepony.mc.otm.capability.MatteryPlayerCapability
import ru.dbotthepony.mc.otm.capability.energy.extractEnergyExact
import ru.dbotthepony.mc.otm.client.render.ResearchIcons
import ru.dbotthepony.mc.otm.core.math.Vector
import ru.dbotthepony.mc.otm.core.util.formatPower
import ru.dbotthepony.mc.otm.core.math.getEllipsoidBlockPositions
import ru.dbotthepony.mc.otm.core.getEntitiesInEllipsoid
import ru.dbotthepony.mc.otm.core.getExplosionResistance
import ru.dbotthepony.mc.otm.core.math.minus
import ru.dbotthepony.mc.otm.core.math.plus
import ru.dbotthepony.mc.otm.core.position
import ru.dbotthepony.mc.otm.core.math.roundToIntVector
import ru.dbotthepony.mc.otm.core.math.times
import ru.dbotthepony.mc.otm.network.MatteryPlayerNetworkChannel
import ru.dbotthepony.mc.otm.capability.matteryPlayer
import ru.dbotthepony.mc.otm.client.ShiftPressedCond
import ru.dbotthepony.mc.otm.core.damageType
import ru.dbotthepony.mc.otm.core.math.RGBAColor
import ru.dbotthepony.mc.otm.network.MatteryPacket
import ru.dbotthepony.mc.otm.network.ShockwaveEffectPacket
import ru.dbotthepony.mc.otm.network.enqueueWork
import ru.dbotthepony.mc.otm.network.packetHandled
import ru.dbotthepony.mc.otm.network.sender
import ru.dbotthepony.mc.otm.onceServer
import ru.dbotthepony.mc.otm.registry.AndroidFeatures
import ru.dbotthepony.mc.otm.registry.MDamageTypes
import ru.dbotthepony.mc.otm.registry.MNames
import ru.dbotthepony.mc.otm.registry.MSoundEvents
import ru.dbotthepony.mc.otm.registry.MatteryDamageSource
import ru.dbotthepony.mc.otm.triggers.ShockwaveDamageMobTrigger
import ru.dbotthepony.mc.otm.triggers.ShockwaveTrigger
import java.util.function.Supplier
import kotlin.math.pow
import kotlin.math.roundToInt

object TriggerShockwavePacket : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		// no op
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.enqueueWork {
			val shockwave = context.sender?.matteryPlayer?.getFeature(AndroidFeatures.SHOCKWAVE) as ShockwaveFeature? ?: return@enqueueWork

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

class ShockwaveFeature(capability: MatteryPlayerCapability) : AndroidSwitchableFeature(AndroidFeatures.SHOCKWAVE, capability) {
	override val maxCooldown: Int
		get() = AndroidConfig.Shockwave.COOLDOWN

	private var wasMidair = false
	private var highestSpeed = 0.0
	var airTicks = 0
		private set

	private var creativeFlightTicks = 0

	override fun tickClient() {
		if (
			!ply.isSpectator &&
			isActive &&
			ply.isShiftKeyDown &&
			!isOnCooldown &&
			android.androidEnergy.extractEnergyExact(AndroidConfig.Shockwave.ENERGY_COST, true) &&
			ply.deltaMovement.y < -0.01 &&
			creativeFlightTicks == 0
		) {
			ply.deltaMovement += Vector(0.0, -AndroidConfig.Shockwave.ACCELERATION / 20.0, 0.0)
		}

		ticker(true)
	}

	fun shockwave() {
		if (ply.isSpectator || isOnCooldown || !android.androidEnergy.extractEnergyExact(AndroidConfig.Shockwave.ENERGY_COST, false)) {
			return
		}

		putOnCooldown()

		if (ply is ServerPlayer) {
			ShockwaveTrigger.trigger(ply as ServerPlayer)
			MatteryPlayerNetworkChannel.sendTrackingAndSelf(ply, ShockwaveEffectPacket(ply.position))

			ply.level().playSound(
				null,
				ply,
				MSoundEvents.ANDROID_SHOCKWAVE,
				SoundSource.PLAYERS,
				1f,
				1f
			)
		}

		// TODO: raycasting
		val entities = ply.level().getEntitiesInEllipsoid(
			ply.position,
			Vector(AndroidConfig.Shockwave.RADIUS_HORIZONTAL, AndroidConfig.Shockwave.RADIUS_VERTICAL, AndroidConfig.Shockwave.RADIUS_HORIZONTAL),
			except = ply,
		) { (it !is LivingEntity || !it.isSpectator && it.isAlive) }

		val wardens = ply.level().getEntitiesInEllipsoid(
			Warden::class.java,
			ply.position,
			Vector(AndroidConfig.Shockwave.RADIUS_HORIZONTAL_WARDEN, AndroidConfig.Shockwave.RADIUS_VERTICAL_WARDEN, AndroidConfig.Shockwave.RADIUS_HORIZONTAL_WARDEN),
		) { true }

		val seen = ReferenceArraySet<Entity>()

		for ((entity, distanceMultiplier) in wardens) {
			seen.add(entity)
			val multiplier = (1.0 - distanceMultiplier).pow(0.5)

			val source = MatteryDamageSource(ply.level().registryAccess().damageType(MDamageTypes.SHOCKWAVE), ply)
			val damage = multiplier.toFloat() * AndroidConfig.Shockwave.DAMAGE.toFloat() * AndroidConfig.Shockwave.WARDEN_DAMAGE_MULT.toFloat()
			entity.hurt(source, damage)
			entity.deltaMovement += (entity.position - ply.position).normalize() * (multiplier * 3.0)

			if (ply is ServerPlayer) {
				ShockwaveDamageMobTrigger.trigger(ply as ServerPlayer, entity, damage, source)
			}
		}

		for ((entity, distanceMultiplier) in entities) {
			if (seen.add(entity)) {
				val multiplier = (1.0 - distanceMultiplier).pow(1.25)

				// don't hurt items, arrows, etc etc
				if (entity is LivingEntity) {
					val source = MatteryDamageSource(ply.level().registryAccess().damageType(MDamageTypes.SHOCKWAVE), ply)
					val damage = multiplier.toFloat() * AndroidConfig.Shockwave.DAMAGE.toFloat()
					entity.hurt(source, damage)
					entity.deltaMovement += (entity.position - ply.position).normalize() * (multiplier * 3.0)

					if (ply is ServerPlayer) {
						ShockwaveDamageMobTrigger.trigger(ply as ServerPlayer, entity, damage, source)
					}
				} else {
					entity.deltaMovement += (entity.position - ply.position).normalize() * (multiplier * 6.0)
				}
			}
		}

		if (AndroidConfig.Shockwave.BREAK_BLOCKS) {
			val rounded = ply.position.roundToIntVector()

			for (blockPos in getEllipsoidBlockPositions(AndroidConfig.Shockwave.RADIUS_HORIZONTAL.roundToInt(), AndroidConfig.Shockwave.RADIUS_VERTICAL.roundToInt(), AndroidConfig.Shockwave.RADIUS_HORIZONTAL.roundToInt())) {
				val newBlockPos = blockPos + rounded

				val blockState = ply.level().getBlockState(newBlockPos)

				if (!blockState.isAir && blockState.getExplosionResistance(ply.level(), newBlockPos) <= 0f && ply.level().getBlockEntity(newBlockPos) == null && blockState.block.defaultDestroyTime() >= 0f) {
					// Block.dropResources(blockState, ply.level(), newBlockPos)
					// blockState.block.destroy(ply.level(), newBlockPos, blockState)
					// ply.level().setBlock(newBlockPos, blockState.fluidState.createLegacyBlock(), Block.UPDATE_ALL)

					ply.level().destroyBlock(newBlockPos, true)
				}
			}
		}
	}

	private fun ticker(isClient: Boolean) {
		if (!ply.onGround() || ply.isSpectator) {
			airTicks = (airTicks + 1).coerceAtMost(3)
		} else {
			airTicks = (airTicks - 1).coerceAtLeast(0)
		}

		if (ply.abilities.flying) {
			creativeFlightTicks = 8
		} else {
			creativeFlightTicks = (creativeFlightTicks - 1).coerceAtLeast(0)
		}

		if (
			!ply.isSpectator &&
			isActive &&
			!isOnCooldown &&
			android.androidEnergy.extractEnergyExact(AndroidConfig.Shockwave.ENERGY_COST, true) &&
			creativeFlightTicks == 0
		) {
			val old = wasMidair
			wasMidair = !ply.onGround()

			if (wasMidair) {
				highestSpeed = (-ply.deltaMovement.y).coerceAtLeast(highestSpeed)
			}

			if (old != wasMidair && !wasMidair && AndroidConfig.Shockwave.TERMINAL_VELOCITY <= (highestSpeed * 20.0) && ply.isShiftKeyDown) {
				if (isClient) {
					// I HATE SELF-UPDATING PLAYERS
					// I HATE SELF-UPDATING PLAYERS
					// fix "bug" where shockwave doesn't trigger even when player is falling faster than orbiting satellite
					putOnCooldown()
					MatteryPlayerNetworkChannel.sendToServer(TriggerShockwavePacket)
				} else {
					shockwave()
				}
			}

			if (!wasMidair) {
				highestSpeed = 0.0
			}
		} else if (!isActive) {
			highestSpeed = 0.0
			wasMidair = false
		}
	}

	override fun tickServer() {
		super.tickServer()

		ticker(false)
	}

	override fun renderIcon(graphics: GuiGraphics, x: Float, y: Float, width: Float, height: Float, color: RGBAColor) {
		ResearchIcons.ICON_SHOCKWAVE.render(graphics, x, y, width, height, color = if (isOnCooldown) color * RGBAColor.REDDISH else color)
	}
}
