package ru.dbotthepony.mc.otm.core.multiblock

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import it.unimi.dsi.fastutil.objects.Reference2IntMap
import it.unimi.dsi.fastutil.objects.Reference2IntMaps
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.LevelAccessor
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.chunk.LevelChunk
import net.minecraft.world.phys.AABB
import ru.dbotthepony.kommons.util.Listenable
import ru.dbotthepony.mc.otm.core.collect.collect
import ru.dbotthepony.mc.otm.core.collect.map
import ru.dbotthepony.mc.otm.core.getBlockEntityNow
import ru.dbotthepony.mc.otm.core.getBlockStateNowOrNull
import ru.dbotthepony.mc.otm.core.math.Vector
import ru.dbotthepony.mc.otm.core.math.compareTo
import ru.dbotthepony.mc.otm.core.math.plus
import ru.dbotthepony.mc.otm.core.registryName
import ru.dbotthepony.mc.otm.network.syncher.IRemoteState
import ru.dbotthepony.mc.otm.network.syncher.ISynchable
import java.io.Closeable
import java.util.LinkedList
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.collections.ArrayDeque
import kotlin.collections.ArrayList
import kotlin.collections.HashMap

/**
 * [close] MUST be called when multiblock goes out of scope
 */
class ShapedMultiblock(pos: BlockPos, factory: ShapedMultiblockFactory) : IMultiblockAccess, ISynchable, Closeable, GlobalBlockEntityRemovalListener {
	override var isValid: MultiblockStatus = MultiblockStatus.INVALID
		private set

	private val customChecks = factory.customChecks
	private val north = Config(Direction.NORTH, pos, factory.north)
	private val south = Config(Direction.SOUTH, pos, factory.south)
	private val west = Config(Direction.WEST, pos, factory.west)
	private val east = Config(Direction.EAST, pos, factory.east)
	private val configurations = ImmutableList.of(north, south, west, east)

	override var generation: Int = 0
		private set

	private val changeListeners = CopyOnWriteArrayList<ChangeListener>()

	private inner class ChangeListener(val runnable: Runnable) : Listenable.L {
		init {
			changeListeners.add(this)
		}

		override fun remove() {
			changeListeners.remove(this)
		}
	}

	override fun onGenerationChanges(listener: Runnable): Listenable.L {
		return ChangeListener(listener)
	}

	override val hasRemotes: Boolean
		get() = remotes.isNotEmpty()

	private inner class Config(override val currentDirection: Direction, val pos: BlockPos, parts: Collection<ShapedMultiblockFactory.Part>) : IMultiblockAccess, ISynchable, GlobalBlockEntityRemovalListener, Comparable<Config> {
		private inner class Part private constructor(override val pos: BlockPos, val prototype: ShapedMultiblockFactory.Part, val chunkPos: ChunkPos) : Comparable<Part>, IMultiblockNode {
			constructor(pos: BlockPos, prototype: ShapedMultiblockFactory.Part) : this(pos, prototype, ChunkPos(pos))

			var index = -1

			private var blockEntity: BlockEntity? = null
			private var blockState: BlockState? = null
			private val assignedBlockEntityLists = ArrayList<BlockEntitySet<*>>(prototype.blockEntityTags.size)
			private val assignedBlockStateLists = ArrayList<Reference2IntMap<BlockState>>()
			private val assignedBlockLists = ArrayList<Reference2IntMap<Block>>()
			private val children: ImmutableList<Part> by lazy(LazyThreadSafetyMode.NONE) { prototype.children.iterator().map { Part(pos, it, chunkPos) }.collect(ImmutableList.toImmutableList()) }

			override fun compareTo(other: Part): Int {
				return chunkPos.compareTo(other.chunkPos)
			}

			init {
				prototype.blockEntityTags.forEach {
					assignedBlockEntityLists.add(getBlockEntityList(it))
				}

				prototype.blockStateTags.forEach {
					assignedBlockStateLists.add(getBlockStateList(it))
				}

				prototype.blockTags.forEach {
					assignedBlockLists.add(getBlockList(it))
				}
			}

			private var lastSuccessfulPathPredicate = -1
			private var lastSuccessfulPathChildren = -1

			override val displayBlocks: List<BlockState> = prototype.displayBlocks.asList()

			override var status: NodeStatus = NodeStatus.UNKNOWN
				private set

			fun read(stream: RegistryFriendlyByteBuf) {
				status = NodeStatus.entries[stream.readUnsignedByte().toInt()]
			}

			fun write(stream: RegistryFriendlyByteBuf) {
				stream.writeByte(status.ordinal)
			}

			private fun orPredicates(levelAccessor: LevelAccessor, blockState: BlockState, blockEntity: Lazy<BlockEntity?>): Boolean {
				if (prototype.predicate.isNotEmpty()) {
					if (lastSuccessfulPathPredicate != -1 && !prototype.predicate[lastSuccessfulPathPredicate].test(pos, levelAccessor, blockState, blockEntity)) {
						lastSuccessfulPathPredicate = -1
					}

					if (lastSuccessfulPathPredicate == -1) {
						lastSuccessfulPathPredicate = prototype.predicate.indexOfFirst { it.test(pos, levelAccessor, blockState, blockEntity) }
						return lastSuccessfulPathPredicate != -1
					}
				}

				return true
			}

			private fun orChildren(levelAccessor: LevelAccessor, blockState: BlockState, blockEntity: Lazy<BlockEntity?>): Boolean {
				if (children.isNotEmpty()) {
					if (lastSuccessfulPathChildren != -1 && !children[lastSuccessfulPathChildren].test0(levelAccessor, blockState, blockEntity)) {
						lastSuccessfulPathChildren = -1
					}

					if (lastSuccessfulPathChildren == -1) {
						lastSuccessfulPathChildren = children.indexOfFirst { it.test0(levelAccessor, blockState, blockEntity) }
						return lastSuccessfulPathChildren != -1
					}
				}

				return true
			}

			private fun test0(levelAccessor: LevelAccessor, blockState: BlockState, blockEntity: Lazy<BlockEntity?>): Boolean {
				val test = when (prototype.strategy) {
					Strategy.OR_BOTH -> orPredicates(levelAccessor, blockState, blockEntity) && orChildren(levelAccessor, blockState, blockEntity)

					Strategy.OR_EITHER ->
						prototype.predicate.isNotEmpty() && orPredicates(levelAccessor, blockState, blockEntity) ||
							children.isNotEmpty() && orChildren(levelAccessor, blockState, blockEntity) ||
							children.isEmpty() && prototype.predicate.isEmpty()

					Strategy.AND -> prototype.predicate.all { it.test(pos, levelAccessor, blockState, blockEntity) } && children.all { it.test0(levelAccessor, blockState, blockEntity) }
				}

				if (test) {
					if (assignedBlockEntityLists.isNotEmpty()) {
						val be1 = this.blockEntity
						val be2 = blockEntity.value

						if (be1 != be2) {
							generation++

							if (be1 != null) {
								if (be1 is IMultiblockListener)
									referencedListeners.dereference(be1)

								assignedBlockEntityLists.forEach { it.remove(be1) }
							}

							if (be2 != null) {
								if (be2 is IMultiblockListener)
									referencedListeners.reference(be2)

								assignedBlockEntityLists.forEach { it.add(be2) }
							}

							this.blockEntity = be2
						}
					}

					if (assignedBlockStateLists.isNotEmpty() || assignedBlockLists.isNotEmpty()) {
						val state = blockState
						val old = this.blockState

						if (state != old) {
							generation++

							if (old != null) {
								assignedBlockStateLists.forEach {
									it[old] = it.getInt(old) - 1
									check(it.getInt(old) >= 0) { "Consistency check failed: Counter for block state $old turned negative" }
								}

								assignedBlockLists.forEach {
									it[old.block] = it.getInt(old.block) - 1
									check(it.getInt(old.block) >= 0) { "Consistency check failed: Counter for block ${old.block.registryName} turned negative" }
								}
							}

							assignedBlockStateLists.forEach {
								it[state] = it.getInt(state) + 1
							}

							assignedBlockLists.forEach {
								it[state.block] = it.getInt(state.block) + 1
							}

							this.blockState = state
						}
					}
				} else {
					clearFull()
				}

				return test
			}

			fun test(levelAccessor: LevelAccessor): MultiblockStatus {
				val blockEntity = lazy(LazyThreadSafetyMode.NONE) { levelAccessor.getBlockEntityNow(pos) }
				val blockState = levelAccessor.getBlockStateNowOrNull(pos) ?: return MultiblockStatus.NOT_LOADED
				return test(levelAccessor, blockState, blockEntity)
			}

			fun test(levelAccessor: LevelAccessor, chunk: LevelChunk): MultiblockStatus {
				val blockEntity = lazy(LazyThreadSafetyMode.NONE) { chunk.getBlockEntity(pos) }
				val blockState = chunk.getBlockState(pos)
				return test(levelAccessor, blockState, blockEntity)
			}

			fun test(levelAccessor: LevelAccessor, blockState: BlockState, blockEntity: Lazy<BlockEntity?>): MultiblockStatus {
				val status = test0(levelAccessor, blockState, blockEntity)

				val previous = this.status
				this.status = if (status) NodeStatus.VALID else NodeStatus.INVALID

				if (previous !== this.status && index != -1) {
					pushNetworkPartUpdate(this)
				}

				return if (status) MultiblockStatus.VALID else MultiblockStatus.INVALID
			}

			private fun clearFull() {
				generation++
				val blockEntity = blockEntity

				if (blockEntity != null) {
					if (blockEntity is IMultiblockListener)
						referencedListeners.dereference(blockEntity)

					assignedBlockEntityLists.forEach { it.remove(blockEntity) }
				}

				val blockState = blockState

				if (blockState != null) {
					assignedBlockStateLists.forEach {
						it[blockState] = it.getInt(blockState) - 1
						check(it.getInt(blockState) >= 0) { "Consistency check failed: Counter for block state $blockState turned negative" }
					}

					assignedBlockLists.forEach {
						it[blockState.block] = it.getInt(blockState.block) - 1
						check(it.getInt(blockState.block) >= 0) { "Consistency check failed: Counter for block ${blockState.block.registryName} turned negative" }
					}
				}

				this.blockEntity = null
				this.blockState = null

				lastSuccessfulPathPredicate = -1
				lastSuccessfulPathChildren = -1

				// avoid allocating iterator when empty
				if (children.isNotEmpty())
					children.forEach { it.clearFull() }
			}

			fun clear() {
				generation++
				val blockEntity = blockEntity

				if (blockEntity != null) {
					assignedBlockEntityLists.forEach { it.remove(blockEntity) }
				}

				this.blockEntity = null
				blockState = null

				lastSuccessfulPathPredicate = -1
				lastSuccessfulPathChildren = -1

				children.forEach { it.clear() }
			}
		}

		private val referencedListeners = MultiblockListenerSet(this)

		override var generation: Int
			get() = this@ShapedMultiblock.generation
			private set(value) { this@ShapedMultiblock.generation = value }

		override fun onGenerationChanges(listener: Runnable): Listenable.L {
			return this@ShapedMultiblock.onGenerationChanges(listener)
		}

		val index = when (currentDirection) {
			Direction.NORTH -> 0
			Direction.SOUTH -> 1
			Direction.WEST -> 2
			Direction.EAST -> 3
			else -> throw RuntimeException()
		}

		override val currentNodes: Map<BlockPos, IMultiblockNode>
			get() = parts

		override val boundingBox: AABB by lazy {
			AABB(
				Vector(parts.minOf { it.pos.x }.toDouble(), parts.minOf { it.pos.y }.toDouble(), parts.minOf { it.pos.z }.toDouble()),
				Vector(parts.maxOf { it.pos.x + 1.0 }, parts.maxOf { it.pos.y + 1.0 }, parts.maxOf { it.pos.z + 1.0 }),
			)
		}

		private val tag2BlockEntity = HashMap<BlockEntityTag<*>, BlockEntitySet<*>>()
		private val tag2BlockState = HashMap<Any, Reference2IntMap<BlockState>>()
		private val tag2Block = HashMap<Any, Reference2IntMap<Block>>()

		private val tag2BlockStateViews = HashMap<Any, Reference2IntMap<BlockState>>()
		private val tag2BlockViews = HashMap<Any, Reference2IntMap<Block>>()

		init {
			getBlockList(IMultiblockAccess.GLOBAL_BLOCK_TAG)
			getBlockStateList(IMultiblockAccess.GLOBAL_BLOCK_TAG)
		}

		private val globalBlocksView = tag2BlockViews[IMultiblockAccess.GLOBAL_BLOCK_TAG]!!
		private val globalBlockStatesView = tag2BlockStateViews[IMultiblockAccess.GLOBAL_BLOCK_TAG]!!

		private fun <T : Any> getBlockEntityList(tag: BlockEntityTag<T>): BlockEntitySet<T> {
			val existing = tag2BlockEntity[tag]

			if (existing != null)
				return existing as BlockEntitySet<T>

			val new = BlockEntitySet(this, tag)
			tag2BlockEntity[tag] = new
			return new
		}

		private fun getBlockList(tag: Any): Reference2IntMap<Block> {
			val existing = tag2Block[tag]

			if (existing != null)
				return existing

			val new = Reference2IntOpenHashMap<Block>()
			tag2Block[tag] = new
			tag2BlockViews[tag] = Reference2IntMaps.unmodifiable(new)
			return new
		}

		private fun getBlockStateList(tag: Any): Reference2IntMap<BlockState> {
			val existing = tag2BlockState[tag]

			if (existing != null)
				return existing

			val new = Reference2IntOpenHashMap<BlockState>()
			tag2BlockState[tag] = new
			tag2BlockStateViews[tag] = Reference2IntMaps.unmodifiable(new)
			return new
		}

		val parts: ImmutableMap<BlockPos, Part> = parts.stream()
			.map { Part(it.pos + pos, it) }
			.sorted() // group/localize parts by in-world chunks to maximize getChunk() cache hit rate
			.collect(ImmutableMap.toImmutableMap({ it.pos }, { it }))

		private val partsByChunk: ImmutableMap<ChunkPos, ImmutableList<Part>>

		init {
			val builder = HashMap<ChunkPos, ArrayList<Part>>()

			for (part in this.parts.values) {
				builder.computeIfAbsent(part.chunkPos) { ArrayList() }.add(part)
			}

			partsByChunk = builder.entries.stream()
				.map { it.key to ImmutableList.copyOf(it.value) }
				.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second }))
		}

		init {
			for ((i, part) in this.parts.values.withIndex()) {
				part.index = i
			}
		}

		fun clear() {
			generation++
			referencedListeners.clear()
			tag2BlockEntity.values.forEach { it.clear() }
			tag2BlockState.values.forEach { it.clear() }
			tag2Block.values.forEach { it.clear() }
			parts.values.forEach { it.clear() }
		}

		private val networkChangelog = ArrayDeque<Part>()
		private var networkVersion = 0

		private fun pushNetworkPartUpdate(part: Part) {
			networkChangelog.addFirst(part)
			networkVersion++
			while (networkChangelog.size > parts.size) networkChangelog.removeLast()
		}

		override var isValid: MultiblockStatus = MultiblockStatus.INVALID
			private set(value) {
				if (value != field) {
					field = value
					generation++
				}
			}

		private var validParts = 0

		override fun compareTo(other: Config): Int {
			var cmp = validParts.compareTo(other.validParts)
			if (cmp == 0) cmp = currentDirection.compareTo(other.currentDirection)
			return cmp
		}

		fun update(levelAccessor: LevelAccessor, updateEverything: Boolean = remotes.isNotEmpty()): MultiblockStatus {
			val networkVersion = networkVersion

			isValid = MultiblockStatus.VALID
			validParts = 0

			for ((chunkPos, parts) in partsByChunk) {
				val chunk = levelAccessor.chunkSource.getChunkNow(chunkPos.x, chunkPos.z)

				if (chunk == null) {
					isValid = MultiblockStatus.NOT_LOADED
					break
				}

				if (updateEverything) {
					for (part in parts) {
						val status = part.test(levelAccessor, chunk)
						if (status === MultiblockStatus.VALID) validParts++
						isValid = isValid.or(status)
					}
				} else {
					for (part in parts) {
						val status = part.test(levelAccessor, chunk)

						if (status === MultiblockStatus.VALID)
							validParts++
						else {
							isValid = status
							break
						}
					}
				}
			}

			if (isValid != MultiblockStatus.VALID) clear()

			if (this.networkVersion != networkVersion) {
				remotes.forEach { it.listener.run() }
			}

			return isValid
		}

		override fun <T : Any> blockEntities(tag: BlockEntityTag<T>): Set<T> {
			checkAccess()
			return (tag2BlockEntity[tag]?.set ?: setOf()) as Set<T>
		}

		override fun blocks(tag: Any): Reference2IntMap<Block> {
			checkAccess()
			return tag2BlockViews[tag] ?: Reference2IntMaps.emptyMap()
		}

		override fun blockStates(tag: Any): Reference2IntMap<BlockState> {
			checkAccess()
			return tag2BlockStateViews[tag] ?: Reference2IntMaps.emptyMap()
		}

		override fun blocks(): Reference2IntMap<Block> {
			checkAccess()
			return globalBlocksView
		}

		override fun blockStates(): Reference2IntMap<BlockState> {
			checkAccess()
			return globalBlockStatesView
		}

		override fun blockEntityRemoved(blockEntity: BlockEntity): Boolean {
			var any = false
			tag2BlockEntity.values.forEach { any = it.blockEntityRemoved(blockEntity) || any }
			return any
		}

		private val remotes = CopyOnWriteArrayList<RemoteState>()

		override val hasRemotes: Boolean
			get() = remotes.isNotEmpty()

		override var isRemote: Boolean = false
			private set

		override fun read(stream: RegistryFriendlyByteBuf) {
			isRemote = true

			if (stream.readBoolean()) {
				 for (part in parts.values) {
					part.read(stream)
				 }
			} else {
				var index = stream.readVarInt()
				val list = parts.values.asList()

				while (index != 0) {
					list[index - 1].read(stream)
					index = stream.readVarInt()
				}
			}
		}

		private inner class RemoteState(val listener: Runnable) : IRemoteState {
			init {
				remotes.add(this)
			}

			private var version = -1

			override fun write(stream: RegistryFriendlyByteBuf) {
				if (version == -1 || networkVersion - version >= networkChangelog.size) {
					stream.writeBoolean(true)

					for (part in parts.values) {
						part.write(stream)
					}
				} else {
					stream.writeBoolean(false)
					val itr = networkChangelog.listIterator(networkVersion - version)

					while (itr.hasPrevious()) {
						val part = itr.previous()
						stream.writeVarInt(part.index + 1)
						part.write(stream)
					}

					stream.writeByte(0)
				}

				version = networkVersion
			}

			override fun invalidate() {
				version = -1
			}

			override fun close() {
				remotes.remove(this)
			}
		}

		override fun createRemoteState(listener: Runnable): IRemoteState {
			return RemoteState(listener)
		}

		private fun checkAccess() {
			if (!isUpdating && activeConfig !== this) {
				throw IllegalStateException("ShapedMultiblock configuration leaked! $this of ${this@ShapedMultiblock}")
			}
		}
	}

	override var isRemote: Boolean = false
		private set

	private var isUpdating = false

	override fun read(stream: RegistryFriendlyByteBuf) {
		isRemote = true
		isValid = MultiblockStatus.entries[stream.readByte().toInt()]
		activeConfig = configurations[stream.readUnsignedByte().toInt()]
		activeConfig.read(stream)
	}

	private val remotes = CopyOnWriteArrayList<RemoteState>()

	private inner class RemoteState(val listener: Runnable) : IRemoteState {
		private val remotes = configurations.map { it.createRemoteState(listener) }

		init {
			this@ShapedMultiblock.remotes.add(this)
		}

		override fun write(stream: RegistryFriendlyByteBuf) {
			stream.writeByte(isValid.ordinal)
			stream.writeByte(activeConfig.index)
			remotes[activeConfig.index].write(stream)
		}

		override fun invalidate() {
			remotes.forEach { it.invalidate() }
		}

		override fun close() {
			remotes.forEach { it.close() }
			this@ShapedMultiblock.remotes.remove(this)
		}
	}

	override fun createRemoteState(listener: Runnable): IRemoteState {
		return RemoteState(listener)
	}

	private var activeConfig: Config = north
		set(value) {
			if (field !== value) {
				field = value
				generation++
			}
		}

	override val currentDirection: Direction?
		get() = if (isValid == MultiblockStatus.VALID) activeConfig.currentDirection else null

	override val boundingBox: AABB?
		get() = if (isValid == MultiblockStatus.VALID) activeConfig.boundingBox else null

	override val currentNodes: Map<BlockPos, IMultiblockNode>
		get() = activeConfig.parts

	override fun <T : Any> blockEntities(tag: BlockEntityTag<T>): Set<T> {
		if (isValid != MultiblockStatus.VALID) return setOf()
		return activeConfig.blockEntities(tag)
	}

	override fun blocks(tag: Any): Reference2IntMap<Block> {
		if (isValid != MultiblockStatus.VALID) return Reference2IntMaps.emptyMap()
		return activeConfig.blocks(tag)
	}

	override fun blockStates(tag: Any): Reference2IntMap<BlockState> {
		if (isValid != MultiblockStatus.VALID) return Reference2IntMaps.emptyMap()
		return activeConfig.blockStates(tag)
	}

	override fun blocks(): Reference2IntMap<Block> {
		if (isValid != MultiblockStatus.VALID) return Reference2IntMaps.emptyMap()
		return activeConfig.blocks()
	}

	override fun blockStates(): Reference2IntMap<BlockState> {
		if (isValid != MultiblockStatus.VALID) return Reference2IntMaps.emptyMap()
		return activeConfig.blockStates()
	}

	override fun blockEntityRemoved(blockEntity: BlockEntity): Boolean {
		return activeConfig.blockEntityRemoved(blockEntity)
	}

	fun update(levelAccessor: LevelAccessor): MultiblockStatus {
		isUpdating = true
		val thisGeneration = generation

		try {
			val configurations = LinkedList<Config>()
			val configurations0 = ArrayList(this.configurations)

			while (configurations0.isNotEmpty()) {
				val max = configurations0.max()
				configurations0.remove(max)
				configurations.add(max)
			}

			isValid = MultiblockStatus.INVALID
			var hadUnloadedChunks = false

			while (configurations.isNotEmpty()) {
				val config = configurations.removeFirst()
				val status = config.update(levelAccessor)

				if (status == MultiblockStatus.VALID) {
					if (customChecks.all { it.test(config) }) {
						activeConfig = config
						isValid = MultiblockStatus.VALID
						remotes.forEach { it.listener.run() }

						if (thisGeneration != generation) {
							changeListeners.forEach { it.runnable.run() }
						}

						return status
					} else {
						config.clear()
					}
				} else if (status == MultiblockStatus.NOT_LOADED) {
					hadUnloadedChunks = true
				}
			}

			if (thisGeneration != generation) {
				changeListeners.forEach { it.runnable.run() }
			}

			isValid = if (hadUnloadedChunks) MultiblockStatus.NOT_LOADED else MultiblockStatus.INVALID
			return isValid
		} finally {
			isUpdating = false
		}
	}

	fun update(levelAccessor: LevelAccessor, direction: Direction): MultiblockStatus {
		isUpdating = true
		val thisGeneration = generation

		try {
			var changes = false

			if (activeConfig.currentDirection != direction && isValid === MultiblockStatus.VALID) {
				activeConfig.clear()
				isValid = MultiblockStatus.INVALID
				changes = true
			}

			val config = when (direction) {
				Direction.NORTH -> north
				Direction.SOUTH -> south
				Direction.WEST -> west
				Direction.EAST -> east
				else -> throw IllegalArgumentException(direction.name)
			}

			changes = changes || activeConfig != config
			activeConfig = config
			isValid = config.update(levelAccessor)

			if (isValid === MultiblockStatus.VALID) {
				isValid = MultiblockStatus.valid(customChecks.all { it.test(config) })
				if (isValid !== MultiblockStatus.VALID) config.clear()
			}

			if (changes) {
				remotes.forEach { it.listener.run() }
			}

			if (thisGeneration != generation) {
				changeListeners.forEach { it.runnable.run() }
			}

			return isValid
		} finally {
			isUpdating = false
		}
	}

	override fun close() {
		changeListeners.clear()
		remotes.forEach { it.close() }
		configurations.forEach { it.clear() }
		isValid = MultiblockStatus.INVALID
	}
}
