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

import net.minecraft.core.BlockPos
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.DoubleTag
import net.minecraft.nbt.ListTag
import net.minecraft.nbt.Tag
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Explosion
import net.minecraft.world.level.ExplosionDamageCalculator
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.material.FluidState
import net.minecraft.world.level.saveddata.SavedData
import net.minecraft.world.phys.Vec3
import net.minecraftforge.event.TickEvent
import net.minecraftforge.eventbus.api.SubscribeEvent
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.block.BlockExplosionDebugger
import ru.dbotthepony.mc.otm.core.*
import ru.dbotthepony.mc.otm.core.math.Vector
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.left
import ru.dbotthepony.mc.otm.core.math.plus
import ru.dbotthepony.mc.otm.core.math.rotateAroundAxis
import ru.dbotthepony.mc.otm.core.math.up
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.registry.MDamageTypes
import ru.dbotthepony.mc.otm.registry.MRegistry
import ru.dbotthepony.mc.otm.registry.MatteryDamageSource
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.math.acos
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt

private fun round(v: Double): Int {
	return (v + 0.5).toInt()
}

private val sphere = arrayOf(
	// ядро
	BlockPos(-1, -1, -1),
	BlockPos(-1, -1, 0),
	BlockPos(-1, -1, 1),
	BlockPos(-1, 0, -1),
	BlockPos(-1, 0, 0),
	BlockPos(-1, 0, 1),
	BlockPos(-1, 1, -1),
	BlockPos(-1, 1, 0),
	BlockPos(-1, 1, 1),
	BlockPos(0, -1, -1),
	BlockPos(0, -1, 0),
	BlockPos(0, -1, 1),
	BlockPos(0, 0, -1),
	BlockPos(0, 0, 0),
	BlockPos(0, 0, 1),
	BlockPos(0, 1, -1),
	BlockPos(0, 1, 0),
	BlockPos(0, 1, 1),
	BlockPos(1, -1, -1),
	BlockPos(1, -1, 0),
	BlockPos(1, -1, 1),
	BlockPos(1, 0, -1),
	BlockPos(1, 0, 0),
	BlockPos(1, 0, 1),
	BlockPos(1, 1, -1),
	BlockPos(1, 1, 0),
	BlockPos(1, 1, 1),

	// столбы
	BlockPos(-2, 0, 0),
	BlockPos(2, 0, 0),
	BlockPos(0, 2, 0),
	BlockPos(0, -2, 0),
	BlockPos(0, 0, 2),
	BlockPos(0, 0, -2),
)

private val initialDirections = arrayOf(
	Vector(1.0, 0.0, 0.0),
	Vector(-1.0, 0.0, 0.0),
	Vector(0.0, 1.0, 0.0),
	Vector(0.0, -1.0, 0.0),
	Vector(0.0, 0.0, 1.0),
	Vector(0.0, 0.0, -1.0),

	Vector(-1.0, -1.0, -1.0).normalize(),
	Vector(1.0, -1.0, 1.0).normalize(),
	Vector(1.0, -1.0, -1.0).normalize(),
	Vector(-1.0, -1.0, 1.0).normalize(),
	Vector(-1.0, 1.0, -1.0).normalize(),
	Vector(1.0, 1.0, 1.0).normalize(),
	Vector(1.0, 1.0, -1.0).normalize(),
	Vector(-1.0, 1.0, 1.0).normalize(),
)

class ExplosionSphere(val hive: ExplosionSphereHive, var pos: Vec3, var stepVelocity: Vec3, var force: Double) {
	val initialPos = pos
	private var lastSplitPos = pos
	val level: ServerLevel get() = hive.level
	val blockPos: BlockPos get() = BlockPos(round(pos.x), round(pos.y), round(pos.z))

	fun travelled(): Double {
		return pos.distanceTo(initialPos)
	}

	private fun travelledFromLastSplit(): Double {
		return pos.distanceTo(lastSplitPos)
	}

	fun step(): Boolean {
		if (force <= 0.0) {
			return false
		}

		val blockPos = blockPos

		for (point in sphere) {
			val finalPos = blockPos + point
			val block = level.getBlockState(finalPos)

			if (!block.isAir && block.block !is BlockExplosionDebugger) {
				val explosion = Explosion(level, null, null, null, pos.x, pos.y, pos.z, force.toFloat(), false, Explosion.BlockInteraction.DESTROY_WITH_DECAY)
				val explosionResistance = block.getExplosionResistance(level, blockPos, explosion)

				if (explosionResistance > force) {
					// поглощено
					// TODO: вместо полного поглощения отражение
					force = 0.0
					return false
				} else {
					// взорвано
					force -= explosionResistance

					// TODO: дропы когда будет добавлена более общая версия
					level.setBlock(blockPos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL)
				}
			}
		}

		pos += stepVelocity

		force -= 0.4

		if (travelledFromLastSplit() >= TRAVEL_TO_SPLIT && force >= 10) {
			force /= 2.0

			val up = stepVelocity.up()
			val left = stepVelocity.left()

			val a = stepVelocity.rotateAroundAxis(up, Math.PI / 4)
			val b = stepVelocity.rotateAroundAxis(up, -Math.PI / 4)
			val c = stepVelocity.rotateAroundAxis(left, Math.PI / 4)
			val d = stepVelocity.rotateAroundAxis(left, -Math.PI / 4)

			hive.addRay(pos, a, force)
			hive.addRay(pos, b, force)
			hive.addRay(pos, c, force)
			hive.addRay(pos, d, force)

			lastSplitPos = pos
		}

		return force > 0.0
	}

	fun serializeNbt(): CompoundTag {
		return CompoundTag()
	}

	companion object {
		const val TRAVEL_TO_SPLIT = 4.0

		fun deserializeNbt(hive: ExplosionSphereHive, tag: CompoundTag): ExplosionSphere {
			return ExplosionSphere(hive, Vector.ZERO, Vector.ZERO, 0.0)
		}
	}
}

class ExplosionSphereHive(val level: ServerLevel) {
	private val spheres = ArrayList<ExplosionSphere>()

	var stepNumber = 0
		private set

	fun addRay(pos: Vec3, stepVelocity: Vec3, force: Double) {
		spheres.add(ExplosionSphere(this, pos, stepVelocity, force))
	}

	fun addDefaultRays(pos: Vec3, force: Double) {
		for (stepVelocity in initialDirections) {
			addRay(pos, stepVelocity, force * 8)
		}
	}

	fun step() {
		stepNumber++

		val toRemove = ArrayList<Int>()

		for (i in 0 until spheres.size) {
			if (!spheres[i].step()) {
				toRemove.add(i)
			}
		}

		for (i in toRemove.size - 1 downTo  0) {
			spheres.removeAt(toRemove[i])
		}
	}

	fun serializeNbt(): CompoundTag {
		return CompoundTag().also {
			it["spheres"] = ListTag().also {
				for (ray in spheres)
					it.add(ray.serializeNbt())
			}
		}
	}

	fun deserializeNbt(tag: CompoundTag) {
		(tag["spheres"] as? ListTag)?.also {
			for (elem in it) {
				spheres.add(ExplosionSphere.deserializeNbt(this, elem as CompoundTag))
			}
		}
	}

	fun isEmpty(): Boolean {
		return spheres.isEmpty()
	}
}

class ExplosionRay(val hive: ExplosionRayHive, var pos: Vec3, var stepVelocity: Vec3, var force: Double) {
	val initialPos = pos
	private var lastSplitPos = pos
	val level: ServerLevel get() = hive.level
	val blockPos: BlockPos get() = BlockPos(round(pos.x), round(pos.y), round(pos.z))
	private var prev: MutableInt? = null

	fun travelled(): Double {
		return pos.distanceTo(initialPos)
	}

	private fun travelledFromLastSplit(): Double {
		return pos.distanceTo(lastSplitPos)
	}

	fun step(spread: Boolean): Boolean {
		if (force <= 0.0) {
			return false
		}

		val blockPos = blockPos

		if (!level.isInWorldBounds(blockPos))
			return false

		// val chunk = level.chunkSource.getChunkNow(SectionPos.blockToSectionCoord(blockPos.x), SectionPos.blockToSectionCoord(blockPos.z)) ?: return true
		val block = level.getBlockState(blockPos)

		if (!block.isAir && block.block !is BlockExplosionDebugger) {
			val explosion = Explosion(level, null, null, null, pos.x, pos.y, pos.z, force.toFloat(), false, Explosion.BlockInteraction.DESTROY_WITH_DECAY)
			val explosionResistance = block.getExplosionResistance(level, blockPos, explosion)

			if (explosionResistance > force) {
				// поглощено
				// TODO: вместо полного поглощения отражение
				force = 0.0
				return false
			} else {
				// взорвано
				force -= explosionResistance

				// TODO: дропы когда будет добавлена более общая версия
				level.setBlock(blockPos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL)
			}
		}

		val old = this.blockPos
		pos += stepVelocity
		val new = this.blockPos

		if (old != new) {
			val prev = prev

			if (prev != null) {
				prev.dec()

				if (prev.value <= 0) {
					hive.checkKey(old)
				}
			}

			this.prev = hive.incDensity(new)
		}

		force -= 0.4

		if (spread && travelledFromLastSplit() >= TRAVEL_TO_SPLIT * sqrt(travelled() / 9.0) && force >= 10) {
			force /= 2.0

			val up = stepVelocity.up()
			val left = stepVelocity.left()

			val a = stepVelocity.rotateAroundAxis(up, Math.PI / 5.5)
			val b = stepVelocity.rotateAroundAxis(up, -Math.PI / 5.5)
			val c = stepVelocity.rotateAroundAxis(left, Math.PI / 5.5)
			val d = stepVelocity.rotateAroundAxis(left, -Math.PI / 5.5)

			hive.addRay(pos, a, force)
			hive.addRay(pos, b, force)
			hive.addRay(pos, c, force)
			hive.addRay(pos, d, force)

			lastSplitPos = pos
		}

		return force > 0f
	}

	fun serializeNbt(): CompoundTag {
		return CompoundTag().also {
			it["pos"] = ListTag().also {
				it.add(DoubleTag.valueOf(pos.x))
				it.add(DoubleTag.valueOf(pos.y))
				it.add(DoubleTag.valueOf(pos.z))
			}

			it["stepVelocity"] = ListTag().also {
				it.add(DoubleTag.valueOf(stepVelocity.x))
				it.add(DoubleTag.valueOf(stepVelocity.y))
				it.add(DoubleTag.valueOf(stepVelocity.z))
			}

			it["force"] = force
		}
	}

	companion object {
		const val TRAVEL_TO_SPLIT = 4.0

		fun deserializeNbt(hive: ExplosionRayHive, tag: CompoundTag): ExplosionRay {
			val pos = tag["pos"] as ListTag
			val stepVelocity = tag["stepVelocity"] as ListTag

			return ExplosionRay(hive,
				Vector((pos[0] as DoubleTag).asDouble, (pos[1] as DoubleTag).asDouble, (pos[2] as DoubleTag).asDouble),
				Vector((stepVelocity[0] as DoubleTag).asDouble, (stepVelocity[1] as DoubleTag).asDouble, (stepVelocity[2] as DoubleTag).asDouble),
				(tag["force"] as DoubleTag).asDouble
			)
		}
	}
}

data class MutableInt(var value: Int = 0) {
	fun inc() {value++}
	fun dec() {value--}
	fun zero() = value <= 0
}

class ExplosionRayHive(val level: ServerLevel) {
	private val rays = ArrayList<ExplosionRay>()
	private val densityMap = HashMap<BlockPos, MutableInt>()
	var stepNumber = 0
		private set

	fun incDensity(pos: BlockPos): MutableInt {
		return densityMap.computeIfAbsent(pos) {MutableInt()}.also(MutableInt::inc)
	}

	fun decDensity(pos: BlockPos) {
		val value = densityMap.computeIfAbsent(pos) {MutableInt()}
		value.dec()

		if (value.zero()) {
			densityMap.remove(pos)
		}
	}

	fun checkKey(pos: BlockPos) {
		val value = densityMap.computeIfAbsent(pos) {MutableInt()}

		if (value.zero()) {
			densityMap.remove(pos)
		}
	}

	fun addRay(pos: Vec3, stepVelocity: Vec3, force: Double) {
		rays.add(ExplosionRay(this, pos, stepVelocity, force))
	}

	fun step() {
		stepNumber++

		val toRemove = ArrayList<Int>()
		val density = calculateDensity()

		for (i in 0 until rays.size) {
			if (!rays[i].step(density < 3.0)) {
				toRemove.add(i)
				decDensity(rays[i].blockPos)
			}
		}

		for (i in toRemove.size - 1 downTo  0) {
			rays.removeAt(toRemove[i])
		}

		if (stepNumber % 4 == 0) {
			LOGGER.info("At step {} density of hive {} with {} elements is {}", stepNumber, this, rays.size, density)
		}
	}

	fun calculateDensity(): Double {
		return rays.size.toDouble() / densityMap.size
	}

	fun serializeNbt(): CompoundTag {
		return CompoundTag().also {
			it["rays"] = ListTag().also {
				for (ray in rays)
					it.add(ray.serializeNbt())
			}
		}
	}

	fun deserializeNbt(tag: CompoundTag) {
		(tag["rays"] as? ListTag)?.also {
			for (elem in it) {
				rays.add(ExplosionRay.deserializeNbt(this, elem as CompoundTag))
			}
		}
	}

	fun isEmpty(): Boolean {
		return rays.isEmpty()
	}

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

		fun evenlyDistributedPoints(amount: Int): List<Vec3> {
			val list = ArrayList<Vec3>()

			for (i in 0 .. amount) {
				val idx = i.toDouble() / amount
				val phi = acos(1.0 - 2.0 * idx)
				val theta = Math.PI * (1.0 + SQUARE_5) * i

				list.add(Vec3(cos(theta) * sin(phi), sin(theta) * sin(phi), cos(phi)))
			}

			return list
		}
	}
}

private object BlackHoleExplosionDamageCalculator : ExplosionDamageCalculator() {
	override fun getBlockExplosionResistance(
		explosion: Explosion,
		getter: BlockGetter,
		pos: BlockPos,
		state: BlockState,
		fstate: FluidState
	): Optional<Float> {
		return if (state.isAir && fstate.isEmpty) Optional.empty() else Optional.of(
			Math.sqrt(
				Math.max(
					0f,
					Math.max(
						state.getExplosionResistance(getter, pos, explosion),
						fstate.getExplosionResistance(getter, pos, explosion)
					)
				).toDouble()
			).toFloat()
		)
	}
}

private data class RingExplosion(val x: Double, val y: Double, val z: Double, val radius: Double, val strength: Float) {
	fun serializeNBT(): CompoundTag {
		return CompoundTag().also {
			it["x"] = x
			it["y"] = y
			it["z"] = z
			it["radius"] = radius
			it["strength"] = strength
		}
	}

	fun explode(queue: ExplosionQueue) {
		for (pos in ExplosionRayHive.evenlyDistributedPoints(radius.toInt() * 80)) {
			val (x, y, z) = pos

			queue.explode(
				this.x + x * radius * 15,
				this.y + y * radius * 15,
				this.z + z * radius * 15,
				strength
			)
		}
	}

	companion object {
		@JvmStatic
		fun deserializeNBT(tag: CompoundTag): RingExplosion {
			return RingExplosion(
				tag.getDouble("x"),
				tag.getDouble("y"),
				tag.getDouble("z"),
				tag.getDouble("radius"),
				tag.getFloat("strength")
			)
		}
	}
}

private data class QueuedExplosion(val x: Double, val y: Double, val z: Double, val radius: Float) {
	fun serializeNBT(): CompoundTag {
		return CompoundTag().also {
			it["x"] = x
			it["y"] = y
			it["z"] = z
			it["radius"] = radius
		}
	}

	fun explode(level: Level) {
		level.explode(
			null,
			MatteryDamageSource(level.registryAccess().damageType(MDamageTypes.HAWKING_RADIATION)),
			BlackHoleExplosionDamageCalculator,
			x,
			y,
			z,
			radius,
			false,
			Level.ExplosionInteraction.BLOCK // TODO: 1.19.3
		)
	}

	companion object {
		fun deserializeNBT(tag: CompoundTag): QueuedExplosion {
			return QueuedExplosion(tag.getDouble("x"), tag.getDouble("y"), tag.getDouble("z"), tag.getFloat("radius"))
		}
	}
}

class ExplosionQueue(private val level: ServerLevel) : SavedData() {
	private var indexExplosion = 0
	private var indexRing = 0
	private val explosions = ArrayList<QueuedExplosion>()
	private val rings = ArrayList<RingExplosion>()
	private val hives = ArrayList<ExplosionRayHive>()

	override fun save(tag: CompoundTag): CompoundTag {
		val listExplosions = ListTag()
		val listRings = ListTag()

		for (i in indexExplosion until explosions.size)
			listExplosions.add(explosions[i].serializeNBT())

		for (i in indexRing until rings.size)
			listRings.add(rings[i].serializeNBT())

		tag["explosions"] = listExplosions
		tag["rings"] = listRings
		// tag["hives"] = ListTag().also {
		// 	for (hive in hives) {
		// 		it.add(hive.serializeNbt())
		// 	}
		// }

		return tag
	}

	fun load(tag: CompoundTag) {
		explosions.clear()
		rings.clear()
		hives.clear()

		indexExplosion = 0
		indexRing = 0

		for (explosion in tag.getList("explosions", Tag.TAG_COMPOUND.toInt()))
			explosions.add(QueuedExplosion.deserializeNBT(explosion as CompoundTag))

		for (ring in tag.getList("rings", Tag.TAG_COMPOUND.toInt()))
			rings.add(RingExplosion.deserializeNBT(ring as CompoundTag))

		for (hive in tag.getList("hives", Tag.TAG_COMPOUND.toInt()))
			hives.add(ExplosionRayHive(level).also { it.deserializeNbt(hive as CompoundTag) })
	}

	fun explode(x: Double, y: Double, z: Double, radius: Float) {
		if (level.isOutsideBuildHeight(BlockPos(x.toInt(), y.toInt() + 24, z.toInt())) || level.isOutsideBuildHeight(BlockPos(x.toInt(), y.toInt() - 24, z.toInt())))
			return

		explosions.add(QueuedExplosion(x, y, z, radius))
		isDirty = true
	}

	fun explodeRing(x: Double, y: Double, z: Double, radius: Double, strength: Float) {
		rings.add(RingExplosion(x, y, z, radius, strength))
		isDirty = true
	}

	fun explodeRays(tpos: Vector, force: Double) {
		val hive = ExplosionRayHive(level)
		hives.add(hive)

		for (normal in ExplosionRayHive.evenlyDistributedPoints(1000)) {
			hive.addRay(normal + tpos, normal, force)
		}

		isDirty = true
	}

	fun tick() {
		if (explosions.size != 0) {
			isDirty = true
			var iterations = 0

			for (i in indexExplosion until explosions.size) {
				explosions[i].explode(level)
				indexExplosion++

				if (iterations++ == 4) {
					break
				}
			}

			if (indexExplosion >= explosions.size) {
				indexExplosion = 0
				explosions.clear()
			}
		} else if (rings.size != 0) {
			if (indexRing >= rings.size) {
				indexRing = 0
				rings.clear()
			} else {
				rings[indexRing++].explode(this)
			}
		}

		for (i in hives.size - 1 downTo 0) {
			isDirty = true
			hives[i].step()

			if (hives[i].isEmpty()) {
				hives.removeAt(i)
			}
		}
	}

	companion object {
		@JvmStatic
		fun queueForLevel(level: ServerLevel): ExplosionQueue {
			return level.dataStorage.computeIfAbsent(
				{
					val factory = ExplosionQueue(level)
					factory.load(it)
					factory
				},
				{ ExplosionQueue(level) },
				"otm_blackhole_explosion_queue"
			)
		}

		@SubscribeEvent
		fun onWorldTick(event: TickEvent.LevelTickEvent) {
			if (event.phase == TickEvent.Phase.START && event.level is ServerLevel) {
				queueForLevel(event.level as ServerLevel).tick()
			}
		}
	}
}
