package ru.dbotthepony.mc.otm.block.entity.blackhole

import it.unimi.dsi.fastutil.objects.ObjectArraySet
import net.minecraft.client.Minecraft
import net.minecraft.core.BlockPos
import net.minecraft.nbt.CompoundTag
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.Entity
import net.minecraft.world.entity.EquipmentSlot
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.entity.boss.wither.WitherBoss
import net.minecraft.world.entity.item.ItemEntity
import net.minecraft.world.entity.player.Player
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.Blocks
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.levelgen.structure.BoundingBox
import net.minecraft.world.phys.AABB
import net.minecraft.world.phys.Vec3
import net.minecraftforge.common.Tags
import net.minecraftforge.registries.ForgeRegistries
import ru.dbotthepony.mc.otm.block.BlackHoleBlock
import ru.dbotthepony.mc.otm.block.entity.tech.GravitationStabilizerBlockEntity
import ru.dbotthepony.mc.otm.block.entity.MatteryBlockEntity
import ru.dbotthepony.mc.otm.block.entity.blackhole.ExplosionQueue.Companion.queueForLevel
import ru.dbotthepony.mc.otm.config.ServerConfig
import ru.dbotthepony.mc.otm.core.damageType
import ru.dbotthepony.mc.otm.core.getExplosionResistance
import ru.dbotthepony.mc.otm.core.gracefulBlockBreak
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.plus
import ru.dbotthepony.mc.otm.registry.MBlockEntities
import ru.dbotthepony.mc.otm.registry.MItems
import ru.dbotthepony.mc.otm.core.math.getSphericalBlockPositions
import ru.dbotthepony.mc.otm.core.math.times
import ru.dbotthepony.mc.otm.core.nbt.map
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.matter.MatterManager
import ru.dbotthepony.mc.otm.registry.MDamageTypes
import ru.dbotthepony.mc.otm.registry.MatteryDamageSource
import ru.dbotthepony.mc.otm.triggers.BlackHoleTrigger
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.sqrt

class BlackHoleBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) : MatteryBlockEntity(MBlockEntities.BLACK_HOLE, p_155229_, p_155230_) {
	var mass by synchronizer.decimal(BASELINE_MASS, setter = setter@{ mass, field, setByRemote ->
		if (setByRemote) {
			field.write(mass)
			return@setter
		}

		if (mass <= Decimal.ZERO) {
			collapse()
			return@setter
		}

		field.write(mass)
		setChanged()
		updateGravStrength()
	})

	var gravitationStrength by synchronizer.double(1.0).property
		private set
	var affectedBounds = BoundingBox(0, 0, 0, 1, 1, 1)
		private set
	var affectedBoundsAABB: AABB = AABB.of(affectedBounds)
		private set

	var spinDirection by synchronizer.bool().property

	private var sphereIterator: Iterator<BlockPos>? = null
	private var sleepTicks = 4
	private val stabilizers = ObjectArraySet<GravitationStabilizerBlockEntity>(6)

	init {
		updateGravStrength()
	}

	override fun setLevel(level: Level) {
		super.setLevel(level)
		sleepTicks = 4
	}

	fun stabilizerAttached(stabilizer: GravitationStabilizerBlockEntity) {
		if (stabilizers.add(stabilizer)) {
			updateGravStrength()
		}
	}

	fun stabilizerDetached(stabilizer: GravitationStabilizerBlockEntity) {
		if (stabilizers.remove(stabilizer)) {
			updateGravStrength()
		}
	}

	fun collapse() {
		val level = level as? ServerLevel ?: return

		level.setBlock(blockPos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL)

		if (gravitationStrength > 0.25) {
			val x0 = blockPos.x + 0.5
			val y0 = blockPos.y + 0.5
			val z0 = blockPos.z + 0.5
			val queue = queueForLevel(level)
			var radius = 0

			while (radius < Math.ceil(gravitationStrength * 4)) {
				queue.explodeRing(
					x0,
					y0,
					z0,
					radius.toDouble(), Math.min(20.0, Math.max(1.0, (gravitationStrength * 4 - radius) * 20)).toFloat()
				)

				radius++
			}
		} else {
			level.explode(
				null,
				MatteryDamageSource(level.registryAccess().damageType(MDamageTypes.HAWKING_RADIATION)),
				null,
				blockPos.x + 0.5,
				blockPos.y + 0.5,
				blockPos.z + 0.5,
				gravitationStrength.toFloat() * 60,
				false,
				Level.ExplosionInteraction.BLOCK // TODO: 1.19.3
			)
		}
	}

	private fun updateGravStrength() {
		var massForDiv = mass

		when (stabilizers.size) {
			0 -> {}
			1 -> massForDiv /= 4
			2 -> massForDiv /= 16
			3 -> massForDiv /= 64
			4 -> massForDiv /= 512
			5 -> massForDiv /= 2048
			else -> massForDiv /= 16384
		}

		gravitationStrength = sqrt(massForDiv.div(BASELINE_MASS).toDouble()).coerceIn(0.2, 40.0)
		affectedBounds = BoundingBox(
			(-30 * gravitationStrength).toInt(),
			(-30 * gravitationStrength).toInt(),
			(-30 * gravitationStrength).toInt(),
			(30 * gravitationStrength).toInt(),
			(30 * gravitationStrength).toInt(),
			(30 * gravitationStrength).toInt()
		).moved(
			blockPos.x, blockPos.y, blockPos.z
		)

		affectedBoundsAABB = AABB.of(affectedBounds)
	}

	override fun getRenderBoundingBox(): AABB {
		return AABB(blockPos.offset(-GravitationStabilizerBlockEntity.RANGE, -GravitationStabilizerBlockEntity.RANGE, -GravitationStabilizerBlockEntity.RANGE), blockPos.offset(
			GravitationStabilizerBlockEntity.RANGE, GravitationStabilizerBlockEntity.RANGE, GravitationStabilizerBlockEntity.RANGE))
	}

	override fun saveLevel(nbt: CompoundTag) {
		super.saveLevel(nbt)
		nbt["mass"] = mass.serializeNBT()
		nbt["spin_direction"] = spinDirection
	}

	override fun load(nbt: CompoundTag) {
		super.load(nbt)
		mass = nbt.map("mass", Decimal::deserializeNBT) ?: BASELINE_MASS
		spinDirection = nbt.getBoolean("spin_direction")
	}

	private fun setDeltaMovement(living: Entity, center: Vec3, distance: Double) {
		//final double mult = Math.min(2, (30 * this.gravitation_strength) / Math.max(1, Math.pow(distance, 2)));
		// Сила притяжения
		var mult = (1 - distance / (30 * gravitationStrength)).coerceAtLeast(0.0).pow(3.0) * gravitationStrength / 16

		if (living is Player) {
			mult *= ServerConfig.Blackhole.PLAYER_FORCE_MULTIPLIER
		} else if (living.type.`is`(Tags.EntityTypes.BOSSES)) {
			mult *= ServerConfig.Blackhole.BOSS_FORCE_MULTIPLIER
		} else {
			mult *= ServerConfig.Blackhole.MOB_FORCE_MULTIPLIER
		}

		// Притяжение к ядру
		val delta = living.position().vectorTo(center).normalize()

		if (distance < gravitationStrength) {
			living.deltaMovement += delta * mult
		} else {
			// Закручивание
			living.deltaMovement += (if (spinDirection) delta.yRot((Math.PI / 2).toFloat()) else delta.yRot((-Math.PI / 2).toFloat())) * (mult * 0.2f) + delta * mult
		}
	}

	fun clientTick() {
		if (--sleepTicks > 0) return
		val ply = Minecraft.getInstance().player ?: return
		val center = Vec3.atCenterOf(blockPos)

		if (!ply.abilities.mayfly && ply.getItemBySlot(EquipmentSlot.CHEST).item != MItems.PORTABLE_GRAVITATION_STABILIZER) {
			val distance = ply.position().distanceTo(center)
			setDeltaMovement(ply, center, distance)
		}

		for (item in level!!.getEntitiesOfClass(ItemEntity::class.java, affectedBoundsAABB)) {
			val distance = item.position().distanceTo(center)
			setDeltaMovement(item, center, distance)
		}
	}

	override fun tick() {
		super.tick()
		if (--sleepTicks > 0) return
		val level = level as? ServerLevel ?: return

		val center = Vec3.atCenterOf(blockPos)

		kotlin.run {
			val gravitationStrength = gravitationStrength.pow(0.5)

			for (living in level.getEntitiesOfClass(LivingEntity::class.java, affectedBoundsAABB)) {
				val distance = living.position().distanceTo(center)

				if (living !is Player || !living.abilities.mayfly && living.getItemBySlot(EquipmentSlot.CHEST).item != MItems.PORTABLE_GRAVITATION_STABILIZER) {
					setDeltaMovement(living, center, distance)

					if (living is ServerPlayer) {
						BlackHoleTrigger.trigger(living)
					}
				}

				if (distance < gravitationStrength + 1.0) {
					val source = MatteryDamageSource(level.registryAccess().damageType(MDamageTypes.EVENT_HORIZON))
					val damage = (gravitationStrength / distance).toFloat()

					if (living is Player) {
						living.hurt(source, damage * ServerConfig.Blackhole.PLAYER_DAMAGE_SCALE.toFloat())
					} else if (living.type.`is`(Tags.EntityTypes.BOSSES)) {
						living.hurt(source, damage * ServerConfig.Blackhole.BOSS_DAMAGE_SCALE.toFloat())
					} else {
						living.hurt(source, damage * ServerConfig.Blackhole.MOB_DAMAGE_SCALE.toFloat())
					}
				}
			}

			for (item in level.getEntitiesOfClass(ItemEntity::class.java, affectedBoundsAABB)) {
				val distance = item.position().distanceTo(center)
				setDeltaMovement(item, center, distance)

				if (distance < gravitationStrength + 1) {
					if (item.hurt(MatteryDamageSource(level.registryAccess().damageType(MDamageTypes.EVENT_HORIZON)), (gravitationStrength / distance).toFloat()) && item.isRemoved) {
						if (item.item.item === MItems.GRAVITATIONAL_DISRUPTOR) {
							collapse()
						} else {
							val mass = MatterManager.get(item.item)

							if (mass.hasMatterValue)
								this.mass += mass.matter
						}
					}
				}
			}
		}

		// шанс 1% что черная дыра потеряет 0.1 MtU каждую секунду * силу гравитации дыры ^ -1
		if (level.random.nextDouble() < 0.01 * 0.05 * (1 / gravitationStrength)) {
			this.mass += HAWKING_MASS_LOSE_STEP
		}

		if (ServerConfig.Blackhole.DESTROY_BLOCKS && gravitationStrength > 0.4) {
			var sphereIterator = sphereIterator

			if (sphereIterator == null || !sphereIterator.hasNext()) {
				sphereIterator = getSphericalBlockPositions((gravitationStrength * 18.0).roundToInt())
				this.sphereIterator = sphereIterator
			}

			var iterations = 0

			while (iterations < ITERATIONS && sphereIterator.hasNext()) {
				iterations++

				val pos = sphereIterator.next() + blockPos
				val getBlock = level.getBlockState(pos)

				if (!getBlock.isAir && getBlock.block !is BlackHoleBlock) {
					val speed = getBlock.getDestroySpeed(level, pos)
					val eResist = getBlock.getExplosionResistance(level, pos)
					var strengthLinear = sqrt(blockPos.distSqr(pos))

					if (strengthLinear < gravitationStrength) {
						strengthLinear *= 1.0 + 8.0 * (gravitationStrength - strengthLinear) / gravitationStrength
					}

					val blockStrength = (speed * 4f).coerceAtLeast(eResist)

					if (speed >= 0f && blockStrength.pow(0.5f).toDouble() < gravitationStrength * (12.0 / strengthLinear)) {
						level.gracefulBlockBreak(pos, getBlock)
					}
				}
			}
		}
	}

	companion object {
		const val ITERATIONS = 30_000
		val BASELINE_MASS = Decimal(50_000)
		val HAWKING_MASS_LOSE_STEP = Decimal(-100)
	}
}
