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

import com.google.common.collect.ImmutableList
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.longs.Long2ObjectFunction
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.SectionPos
import net.minecraft.core.Vec3i
import net.minecraft.nbt.CompoundTag
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.Container
import net.minecraft.world.entity.player.Player
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.chunk.LevelChunk
import net.minecraft.world.phys.Vec3
import net.minecraftforge.common.capabilities.Capability
import net.minecraftforge.common.capabilities.ForgeCapabilities
import net.minecraftforge.common.util.LazyOptional
import net.minecraftforge.energy.IEnergyStorage
import net.minecraftforge.event.TickEvent.LevelTickEvent
import net.minecraftforge.event.entity.player.PlayerEvent
import net.minecraftforge.event.level.ChunkWatchEvent
import net.minecraftforge.event.level.LevelEvent
import net.minecraftforge.event.server.ServerStoppingEvent
import ru.dbotthepony.mc.otm.SERVER_IS_LIVE
import ru.dbotthepony.mc.otm.block.INeighbourChangeListener
import ru.dbotthepony.mc.otm.block.RotatableMatteryBlock
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.energy.IMatteryEnergyStorage
import ru.dbotthepony.mc.otm.core.ISubscriptable
import ru.dbotthepony.mc.otm.core.collect.WeakHashSet
import ru.dbotthepony.mc.otm.core.get
import ru.dbotthepony.mc.otm.core.immutableList
import ru.dbotthepony.mc.otm.core.math.BlockRotation
import ru.dbotthepony.mc.otm.core.math.RelativeSide
import ru.dbotthepony.mc.otm.core.math.minus
import ru.dbotthepony.mc.otm.core.math.plus
import ru.dbotthepony.mc.otm.core.util.IntCounter
import ru.dbotthepony.mc.otm.core.util.Savetables
import ru.dbotthepony.mc.otm.core.util.TickList
import ru.dbotthepony.mc.otm.network.BlockEntitySyncPacket
import ru.dbotthepony.mc.otm.network.GenericNetworkChannel
import ru.dbotthepony.mc.otm.network.synchronizer.FieldSynchronizer
import ru.dbotthepony.mc.otm.once
import ru.dbotthepony.mc.otm.onceServer
import ru.dbotthepony.mc.otm.sometimeServer
import java.lang.ref.WeakReference
import java.util.*
import java.util.function.Consumer
import java.util.function.Predicate
import java.util.function.Supplier
import java.util.stream.Stream
import kotlin.collections.ArrayList
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

/**
 * Absolute barebone (lol) block entity class in Overdrive that Matters, providing bare minimum (lulmao, minecraft engine) functionality
 */
abstract class MatteryBlockEntity(p_155228_: BlockEntityType<*>, p_155229_: BlockPos, p_155230_: BlockState) : BlockEntity(p_155228_, p_155229_, p_155230_), INeighbourChangeListener {
	private var isSynchronizing = false

	/**
	 * "shortcut" for getting [BlockRotation]
	 *
	 * if block has no rotation, returns [BlockRotation.NORTH]
	 */
	open val blockRotation: BlockRotation get() {
		return blockState[(blockState.block as? RotatableMatteryBlock ?: return BlockRotation.NORTH).rotationProperty]
	}

	private val _droppableContainers = ObjectArraySet<Container>()
	private val _neighbourChangeListeners = ObjectArraySet<INeighbourChangeListener>()
	val droppableContainers: Set<Container> = Collections.unmodifiableSet(_droppableContainers)
	val neighbourChangeListeners: Set<INeighbourChangeListener> = Collections.unmodifiableSet(_neighbourChangeListeners)

	protected fun addDroppableContainer(container: Container) {
		_droppableContainers.add(container)
	}

	protected fun addNeighbourListener(listener: INeighbourChangeListener) {
		if (listener === this)
			throw IllegalArgumentException("are you drunk")

		_neighbourChangeListeners.add(listener)
	}

	open fun beforeDroppingItems(oldBlockState: BlockState, level: Level, blockPos: BlockPos, newBlockState: BlockState, movedByPiston: Boolean) {}
	open fun beforeDestroyedByPlayer(level: Level, blockPos: BlockPos, blockState: BlockState, player: Player) {}

	private val _sides = EnumMap<RelativeSide, Side>(RelativeSide::class.java)
	val sides: Map<RelativeSide, Side> = Collections.unmodifiableMap(_sides)

	fun side(side: RelativeSide) = sides[side]!!

	private data class SidelessCap<T : Any>(val cap: T, var optional: LazyOptional<T>)
	private val sidelessCaps = Reference2ObjectOpenHashMap<Capability<*>, SidelessCap<*>>()
	protected val tickList = TickList()
	protected val blockStateChangesCounter = IntCounter()
	protected val dirtyListeners = ISubscriptable.Impl<Unit>()
	private val waitForServerLevel = ArrayList<() -> Unit>()

	/**
	 * Shared savetables, written both to level storage and to item tag
	 */
	protected val savetables = Savetables()

	/**
	 * Level-only savetables, written only to level storage
	 */
	protected val savetablesLevel = Savetables()

	open fun tick() {
		tickList.tick()

		if (synchronizer.isNotEmpty)
			synchronizeToPlayers(false)
	}

	/**
	 * exposes capability when no side is specified
	 */
	protected fun <T : Any> exposeSideless(capability: Capability<T>, value: T) {
		check(!sidelessCaps.containsKey(capability)) { "Already has globally exposed $capability!" }
		sidelessCaps[capability] = SidelessCap(value, LazyOptional.of { value })
		setChanged()
	}

	/**
	 * Exposes capability unconditionally, on all sides and sideless
	 */
	protected fun <T : Any> exposeGlobally(capability: Capability<T>, value: T, predicate: Predicate<RelativeSide> = Predicate { true }) {
		exposeSideless(capability, value)

		for (side in _sides.values)
			if (predicate.test(side.side))
				side.Cap(capability, value)
	}

	protected fun exposeEnergyGlobally(value: IMatteryEnergyStorage, predicate: Predicate<RelativeSide> = Predicate { true }) {
		exposeGlobally(ForgeCapabilities.ENERGY, value, predicate)
		exposeGlobally(MatteryCapability.ENERGY, value, predicate)
	}

	protected fun exposeEnergySideless(value: IMatteryEnergyStorage) {
		exposeSideless(ForgeCapabilities.ENERGY, value)
		exposeSideless(MatteryCapability.ENERGY, value)
	}

	protected fun exposeEnergy(side: RelativeSide, value: IMatteryEnergyStorage): ImmutableList<Side.Cap<*>> {
		return immutableList {
			val thisSide = _sides[side]!!

			accept(thisSide.Cap(ForgeCapabilities.ENERGY, value))
			accept(thisSide.Cap(MatteryCapability.ENERGY, value))
		}
	}

	protected fun waitForServerLevel(lambda: () -> Unit) {
		if (level is ServerLevel) {
			lambda.invoke()
		} else if (level == null) {
			waitForServerLevel.add(lambda)
		}
	}

	interface SideListener<T> : Supplier<LazyOptional<T>>, ISubscriptable<LazyOptional<T>>

	inner class Side(val side: RelativeSide) {
		init {
			check(!_sides.containsKey(side)) { "dafuq are you trying to do" }
			_sides[side] = this
		}

		private val caps = Reference2ObjectOpenHashMap<Capability<*>, Cap<*>>()
		private val subscriptions = Reference2ObjectOpenHashMap<Capability<*>, SubRef<*>>()
		private val knownLOs = WeakHashSet<LazyOptional<*>>()

		private inner class SubRef<T>(value: LazyOptional<T>) : SideListener<T> {
			var value: LazyOptional<T> = value
				set(value) {
					if (value !== field) {
						field = value
						listeners.accept(value)
					}
				}

			private val listeners = ISubscriptable.Impl<LazyOptional<T>>()

			override fun addListener(listener: Consumer<LazyOptional<T>>): ISubscriptable.L {
				val l = listeners.addListener(listener)
				if (level is ServerLevel) listener.accept(value)
				return l
			}

			override fun get(): LazyOptional<T> {
				return value
			}

			fun unset() {
				value = LazyOptional.empty()
			}
		}

		fun <T> track(capability: Capability<T>): SideListener<T> {
			var subref = subscriptions[capability] as SideListener<T>?

			if (subref == null) {
				subref = SubRef(LazyOptional.empty<Any?>()) as SubRef<T>
				subscriptions[capability] = subref
				level?.once { updateTracked(capability) }
			}

			return subref
		}

		fun trackEnergy(): SideListener<IEnergyStorage> {
			return track(ForgeCapabilities.ENERGY)
		}

		fun updateTracked() {
			for (key in subscriptions.keys) {
				// Concurrent Modification safety:
				// we do not add nor remove keys from map, we only update values
				updateTracked(key)
			}
		}

		private fun updateTracked(capability: Capability<*>) {
			if (isRemoved || !SERVER_IS_LIVE) return
			val dir = blockRotation.side2Dir(side)
			val targetPos = blockPos + dir.normal

			val chunk = level
				?.chunkSource
				?.getChunkNow(SectionPos.blockToSectionCoord(targetPos.x), SectionPos.blockToSectionCoord(targetPos.z))

			val subref = subscriptions[capability] as SubRef<Any?>

			if (chunk == null) {
				subref.unset()
				level?.once { updateTracked(capability) }
				return
			}

			val entity = chunk.getBlockEntity(targetPos)

			if (entity == null) {
				subref.unset()
				return
			}

			val new = entity.getCapability(capability, dir.opposite)

			if (!new.isPresent) {
				subref.unset()
				return
			}

			if (subref.value !== new) {
				if (knownLOs.add(new)) {
					val ref = WeakReference(this)

					new.addListener {
						ref.get()?.updateTracked(capability)
					}
				}

				subref.value = new as LazyOptional<Any?>
			}
		}

		operator fun <T : Any> get(capability: Capability<T>): Cap<T>? {
			return caps[capability] as Cap<T>?
		}

		fun invalidate() {
			for (cap in caps.values)
				cap.invalidate()
		}

		fun revive() {
			for (cap in caps.values)
				cap.revive()
		}

		inner class Cap<T : Any>(val type: Capability<in T>, val capability: T) {
			init {
				check(!caps.containsKey(type)) { "Already has capability $type on side $side" }
				caps[type] = this
			}

			var isExposed = true
				private set
			var isValid = true
				private set
			var isRemoved = false
				private set

			var optional: LazyOptional<T> by object : ReadWriteProperty<Any?, LazyOptional<T>> {
				private var value: LazyOptional<T>? = null

				override fun getValue(thisRef: Any?, property: KProperty<*>): LazyOptional<T> {
					if (value == null) {
						value = LazyOptional.of { capability }
					}

					return value!!
				}

				override fun setValue(thisRef: Any?, property: KProperty<*>, value: LazyOptional<T>) {
					this.value = value
				}
			}
				private set

			fun remove() {
				if (!isRemoved) {
					isRemoved = true
					val removed = caps.remove(type)
					check(removed == this) { "$removed != $this" }
					optional.invalidate()
				}
			}

			fun close() {
				if (!isRemoved && isExposed) {
					isExposed = false
					optional.invalidate()

					if (SERVER_IS_LIVE)
						level?.once { setChanged() }
				}
			}

			fun expose() {
				if (!isRemoved && !isExposed) {
					isExposed = true

					if (isValid) {
						optional = LazyOptional.of { capability }

						if (SERVER_IS_LIVE)
							level?.once { setChanged() }
					}
				}
			}

			fun invalidate() {
				if (!isRemoved && isValid) {
					isValid = false
					optional.invalidate()

					if (SERVER_IS_LIVE)
						level?.once { setChanged() }
				}
			}

			fun revive() {
				if (!isRemoved && !isValid) {
					isValid = true

					if (isExposed) {
						optional = LazyOptional.of { capability }

						if (SERVER_IS_LIVE)
							level?.once { setChanged() }
					}
				}
			}
		}
	}

	val front = Side(RelativeSide.FRONT)
	val back = Side(RelativeSide.BACK)
	val left = Side(RelativeSide.LEFT)
	val right = Side(RelativeSide.RIGHT)
	val top = Side(RelativeSide.TOP)
	val bottom = Side(RelativeSide.BOTTOM)

	override fun <T : Any> getCapability(cap: Capability<T>, side: Direction?): LazyOptional<T> {
		if (side != null) {
			return _sides[blockRotation.dir2Side(side)]!![cap]?.optional ?: super.getCapability(cap, side)
		}

		return sidelessCaps[cap]?.optional?.cast() ?: super.getCapability(cap, side)
	}

	override fun invalidateCaps() {
		super.invalidateCaps()

		for (side in sides.values)
			side.invalidate()
	}

	override fun reviveCaps() {
		super.reviveCaps()

		for (side in sides.values)
			side.revive()
	}

	final override fun saveAdditional(nbt: CompoundTag) {
		super.saveAdditional(nbt)
		saveShared(nbt)
		saveLevel(nbt)
	}

	/**
	 * Saved both to item dropped, and to level storage
	 */
	open fun saveShared(nbt: CompoundTag) {
		savetables.serializeNBT(nbt)
	}

	/**
	 * Only saved to level storage, discarded when dropped as item
	 */
	open fun saveLevel(nbt: CompoundTag) {
		savetablesLevel.serializeNBT(nbt)
	}

	override fun load(nbt: CompoundTag) {
		super.load(nbt)
		savetables.deserializeNBT(nbt)
		savetablesLevel.deserializeNBT(nbt)
	}

	@Suppress("OVERRIDE_DEPRECATION")
	override fun setBlockState(pBlockState: BlockState) {
		blockStateChangesCounter.increment()
		val old = blockRotation
		@Suppress("DEPRECATION")
		super.setBlockState(pBlockState)
		val new = blockRotation

		if (old != new) {
			for (side in _sides.values) {
				side.updateTracked()
				side.invalidate()
			}
		}

		for (side in _sides.values) {
			side.revive()
		}
	}

	override fun neighborChanged(state: BlockState, level: Level, pos: BlockPos, neighbour: Block, neighbourPos: BlockPos, movedByPiston: Boolean) {
		_neighbourChangeListeners.forEach { it.neighborChanged(state, level, pos, neighbour, neighbourPos, movedByPiston) }
		val dir = vec2Dir[vecKey(neighbourPos - blockPos)] ?: return
		_sides[blockRotation.dir2Side(dir)]!!.updateTracked()
	}

	override fun setChanged() {
		super.setChanged()

		dirtyListeners.accept(Unit)
	}

	// Just to mark chunk unsaved
	open fun markDirtyFast() {
		val level = level

		if (level is ServerLevel) {
			level.chunkSource.getChunkNow(
				SectionPos.blockToSectionCoord(blockPos.x),
				SectionPos.blockToSectionCoord(blockPos.z))?.isUnsaved = true
		}

		dirtyListeners.accept(Unit)
	}

	val synchronizer = FieldSynchronizer {
		if (isSynchronizing || tickList.ticks != 0)
			return@FieldSynchronizer

		if (isRemoved) markSynchronizerClean()

		if (level?.isClientSide == false && (_subCache == null || (_subCache ?: throw ConcurrentModificationException()).players.isNotEmpty())) {
			ru.dbotthepony.mc.otm.onceServer {
				synchronizeToPlayers(true)
			}
		} else {
			markSynchronizerClean()
		}
	}

	private fun markSynchronizerClean() {
		synchronizer.markClean()
	}

	init {
		synchronizer.defaultEndpoint.markUnused()
	}

	override fun setLevel(level: Level) {
		val old = this.level
		super.setLevel(level)
		unsubscribe()
		_subCache = null

		if (!level.isClientSide) {
			subscribe()

			if (old != null) {
				for (side in _sides.values) {
					side.updateTracked()
					side.invalidate()
				}

				for (side in _sides.values) {
					side.revive()
				}
			} else {
				level.once {
					for (side in _sides.values) {
						side.updateTracked()
					}
				}
			}

			waitForServerLevel.forEach { it.invoke() }
		} else {
			waitForServerLevel.clear()
		}
	}

	override fun setRemoved() {
		super.setRemoved()
		unsubscribe()
	}

	override fun clearRemoved() {
		super.clearRemoved()

		if (level?.isClientSide == false) {
			subscribe()
		}
	}

	private var _subCache: ChunkSubscribers? = null
	private val subscription get() = _subCache ?: subscribe()
	private var playerListUpdated = false

	private fun unsubscribe() {
		val subCache = _subCache ?: return

		if (subCache.unsubscribe(this)) {
			val level = subCache.level.get()

			if (level != null) {
				playerMap.get(level)?.remove(subCache.chunkPos)
			}
		}

		playerListUpdated = false
	}

	private fun subscribe(): ChunkSubscribers {
		val level = level
		check(level is ServerLevel) { "Invalid realm" }
		unsubscribe()

		val subs = playerMap
			.computeIfAbsent(level) { Long2ObjectOpenHashMap() }
			.computeIfAbsent(ChunkPos(blockPos).toLong(), Long2ObjectFunction { ChunkSubscribers(level, ChunkPos(blockPos).toLong()) })

		subs.subscribe(this)
		_subCache = subs
		playerListUpdated = true
		return subs
	}

	private fun synchronizeToPlayers(calledBySynchronizer: Boolean) {
		isSynchronizing = true

		try {
			check(level is ServerLevel) { "Invalid realm or Level is null" }
			synchronizer.observe()
			val subscription = subscription

			if (subscription.players.isNotEmpty() && (playerListUpdated || synchronizer.isDirty)) {
				playerListUpdated = false

				for (player in subscription.players) {
					if (player !in subscription.veto) {
						val payload = synchronizer.computeEndpointFor(player).collectNetworkPayload()

						if (payload != null) {
							GenericNetworkChannel.send(player, BlockEntitySyncPacket(blockPos, payload.array, payload.length))
						}
					}
				}

				synchronizer.markClean()
			} else if (calledBySynchronizer) {
				synchronizer.markClean()
			}
		} finally {
			isSynchronizing = false
		}
	}

	private class ChunkSubscribers(level: ServerLevel, val chunkPos: Long) {
		val level = WeakReference(level)
		val blockEntities = WeakHashSet<MatteryBlockEntity>(linked = true, initialCapacity = 0)
		val players = ObjectLinkedOpenHashSet<ServerPlayer>(0)
		val veto = ObjectLinkedOpenHashSet<ServerPlayer>(0)
		val blockEntitiesWithObservers = WeakHashSet<MatteryBlockEntity>(linked = true, initialCapacity = 0)

		operator fun component1() = blockEntities
		operator fun component2() = players

		val hasObservers: Boolean get() {
			return blockEntities.any { it.synchronizer.hasObservers }
		}

		private fun recheckPlayer(player: ServerPlayer) {
			sometimeServer {
				if (player in players && !player.hasDisconnected()) {
					veto.remove(player)

					blockEntities.forEach {
						it.playerListUpdated = true
						it.synchronizeToPlayers(false)
					}
				} else if (player in players && player.hasDisconnected()) {
					unsubscribe(player)
				}
			}
		}

		fun subscribe(player: ServerPlayer) {
			if (players.add(player)) {
				veto.add(player)
				recheckPlayer(player)
			}
		}

		fun unsubscribe(player: ServerPlayer): Boolean {
			if (players.remove(player)) {
				veto.remove(player)

				blockEntities.forEach {
					it.synchronizer.removeEndpointFor(player)
				}

				return true
			}

			return false
		}

		fun subscribe(blockEntity: MatteryBlockEntity) {
			if (!blockEntities.add(blockEntity)) return

			onceServer {
				blockEntity.synchronizeToPlayers(false)
			}

			if (blockEntity.synchronizer.hasObservers && blockEntity.tickList.ticks == 0) {
				blockEntitiesWithObservers.add(blockEntity)

				tickingMap
					.computeIfAbsent(level.get() ?: throw NullPointerException("Level got GCd!")) { WeakHashSet(linked = true, initialCapacity = 2) }
					.add(this)
			}
		}

		fun unsubscribe(blockEntity: MatteryBlockEntity): Boolean {
			blockEntities.remove(blockEntity)
			blockEntitiesWithObservers.remove(blockEntity)

			return players.isEmpty() && blockEntities.isEmpty()
		}

		val isEmpty: Boolean get() {
			return players.isEmpty() && blockEntities.isEmpty()
		}
	}

	/**
	 * Why track player-tracked chunks?
	 *
	 * because minecraft itself doesn't track them, well,
	 * in "section partitioning" way.
	 *
	 * just look at [net.minecraft.server.level.PlayerMap]
	 *
	 * the [net.minecraft.server.level.PlayerMap.getPlayers] straight ignores chunk position
	 * and just fetches all players
	 *
	 * even if they did not ignore that argument, you still have to fetch player *list* though
	 * [net.minecraft.server.level.ChunkMap.getPlayers], which is not something we want
	 */
	companion object {
		const val REDSTONE_CONTROL_KEY = "redstoneControl"
		const val INVENTORY_KEY = "container"
		const val MATTER_STORAGE_KEY = "matterStorage"
		const val ENERGY_KEY = "energyStorage"
		const val BATTERY_KEY = "batteryInventory"
		const val LOOT_TABLE_KEY = RandomizableContainerBlockEntity.LOOT_TABLE_TAG
		const val LOOT_TABLE_SEED_KEY = RandomizableContainerBlockEntity.LOOT_TABLE_SEED_TAG

		private val playerMap = WeakHashMap<ServerLevel, Long2ObjectOpenHashMap<ChunkSubscribers>>()
		private val tickingMap = WeakHashMap<ServerLevel, WeakHashSet<ChunkSubscribers>>()

		private val vec2Dir = Int2ObjectOpenHashMap<Direction>()

		/**
		 * Returns stream of players watching (tracking) specified [chunkPos] in [level]
		 */
		fun watchingPlayers(chunkPos: Long, level: Level): Stream<ServerPlayer> {
			if (level !is ServerLevel)
				return Stream.empty()

			val subs = playerMap[level] ?: return Stream.empty()
			val chunk = subs[chunkPos] ?: return Stream.empty()
			return chunk.players.stream()
		}

		/**
		 * Returns stream of players watching (tracking) specified [chunkPos] in [level]
		 */
		fun watchingPlayers(chunkPos: ChunkPos, level: Level) = watchingPlayers(chunkPos.toLong(), level)

		/**
		 * Returns stream of players watching (tracking) specified [blockPos] in [level]
		 */
		fun watchingPlayers(blockPos: BlockPos, level: Level) = watchingPlayers(ChunkPos(blockPos), level)

		/**
		 * Returns stream of players watching (tracking) specified [blockPos] in [level]
		 */
		fun watchingPlayers(blockPos: Vec3, level: Level) = watchingPlayers(ChunkPos.asLong(SectionPos.blockToSectionCoord(blockPos.x), SectionPos.blockToSectionCoord(blockPos.z)), level)

		/**
		 * Returns stream of players watching (tracking) specified [chunk]
		 */
		fun watchingPlayers(chunk: LevelChunk) = watchingPlayers(chunk.pos, chunk.level)

		private fun vecKey(value: Vec3i): Int {
			if (value.x !in -1 .. 1) return -1
			if (value.y !in -1 .. 1) return -1
			if (value.z !in -1 .. 1) return -1
			val x = if (value.x < 0) 2 else value.x
			val y = if (value.y < 0) 2 else value.y
			val z = if (value.z < 0) 2 else value.z
			return x or (y shl 4) or (z shl 8)
		}

		init {
			for (dir in Direction.entries) {
				vec2Dir[vecKey(dir.normal)] = dir
			}
		}

		fun onLevelUnload(event: LevelEvent.Unload) {
			val level = event.level as? ServerLevel ?: return
			playerMap.remove(level)
			tickingMap.remove(level)
		}

		fun onServerStopping(event: ServerStoppingEvent) {
			playerMap.clear()
			tickingMap.clear()
		}

		fun postLevelTick(event: LevelTickEvent) {
			val level = event.level as? ServerLevel ?: return
			val ticking = tickingMap[level] ?: return

			ticking.removeIf {
				val shouldRemove = it.blockEntitiesWithObservers.isEmpty()

				if (!shouldRemove && it.players.isNotEmpty()) {
					it.blockEntitiesWithObservers.forEach {
						it.synchronizeToPlayers(false)
					}
				}

				shouldRemove
			}

			if (ticking.isEmpty()) {
				tickingMap.remove(level)
			}
		}

		fun onWatch(event: ChunkWatchEvent.Watch) {
			playerMap
				.computeIfAbsent(event.level) { Long2ObjectOpenHashMap() }
				.computeIfAbsent(event.pos.toLong(), Long2ObjectFunction { ChunkSubscribers(event.level, it) })
				.subscribe(event.player)
		}

		fun onForget(event: ChunkWatchEvent.UnWatch) {
			val levelMap = playerMap[event.level] ?: return
			val subs = levelMap.get(event.pos.toLong()) ?: return

			if (subs.unsubscribe(event.player) && subs.isEmpty) {
				levelMap.remove(event.pos.toLong())
			}
		}

		fun playerDisconnected(event: PlayerEvent.PlayerLoggedOutEvent) {
			for (tree in playerMap.values) {
				tree.values.removeIf {
					it.unsubscribe(event.entity as ServerPlayer)
					it.isEmpty
				}
			}
		}
	}
}
