package ru.dbotthepony.kstarbound import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.JsonObject import it.unimi.dsi.fastutil.ints.Int2ObjectFunction import it.unimi.dsi.fastutil.ints.Int2ObjectMap import it.unimi.dsi.fastutil.ints.Int2ObjectMaps import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.objects.Object2ObjectFunction import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.Either import java.util.Collections import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.locks.ReentrantLock import java.util.function.Supplier import kotlin.collections.set import kotlin.concurrent.withLock class Registry(val name: String) { private val keysInternal = HashMap() private val idsInternal = Int2ObjectOpenHashMap() private val keyRefs = HashMap() private val idRefs = Int2ObjectOpenHashMap() private val backlog = ConcurrentLinkedQueue() // it is much cheaper to queue registry additions rather than locking during high congestion fun add(task: Runnable) { backlog.add(task) } fun finishLoad() { lock.withLock { var next = backlog.poll() while (next != null) { next.run() next = backlog.poll() } } } private val lock = ReentrantLock() val keys: Map> = Collections.unmodifiableMap(keysInternal) val ids: Int2ObjectMap> = Int2ObjectMaps.unmodifiable(idsInternal) sealed class Ref : Supplier?> { abstract val key: Either abstract val entry: Entry? abstract val registry: Registry val isPresent: Boolean get() = value != null val isEmpty: Boolean get() = value == null val value: T? get() = entry?.value final override fun get(): Entry? { return entry } } sealed class Entry : Supplier { abstract val key: String abstract val id: Int? abstract val value: T abstract val json: JsonElement abstract val file: IStarboundFile? abstract val registry: Registry @Deprecated("Careful! This might be confused with 'isMeta' of some entries (such as tiles, in such case use entry.isMeta)") abstract val isBuiltin: Boolean abstract val ref: Ref final override fun get(): T { return value } val jsonObject: JsonObject get() = json as JsonObject } private inner class Impl(override val key: String, override var value: T, override var id: Int? = null) : Entry() { override var json: JsonElement = JsonNull.INSTANCE override var file: IStarboundFile? = null override var isBuiltin: Boolean = false override val ref: Ref by lazy { ref(key) } override fun equals(other: Any?): Boolean { return this === other } override fun hashCode(): Int { return key.hashCode() } override fun toString(): String { return "Registry.Entry[key=$key, id=$id, registry=$name]" } override val registry: Registry get() = this@Registry } private inner class RefImpl(override val key: Either) : Ref() { override var entry: Entry? = null var references = 0 override fun equals(other: Any?): Boolean { return this === other || other is Registry<*>.RefImpl && other.key == key && other.registry == registry } override fun hashCode(): Int { return key.hashCode() } override fun toString(): String { return "Registry.Ref[key=$key, bound to value=${entry != null}, registry=$name]" } override val registry: Registry get() = this@Registry } operator fun get(index: String): Entry? = lock.withLock { keysInternal[index] } operator fun get(index: Int): Entry? = lock.withLock { idsInternal[index] } fun getOrThrow(index: String): Entry { return get(index) ?: throw NoSuchElementException("No such $name: $index") } fun getOrThrow(index: Int): Entry { return get(index) ?: throw NoSuchElementException("No such $name: $index") } fun ref(index: String): Ref = lock.withLock { keyRefs.computeIfAbsent(index, Object2ObjectFunction { val ref = RefImpl(Either.left(it as String)) ref.entry = keysInternal[it] ref }).also { it.references++ } } fun ref(index: Int): Ref = lock.withLock { idRefs.computeIfAbsent(index, Int2ObjectFunction { val ref = RefImpl(Either.right(it)) ref.entry = idsInternal[it] ref }).also { it.references++ } } operator fun contains(index: String) = lock.withLock { index in keysInternal } operator fun contains(index: Int) = lock.withLock { index in idsInternal } fun validate(): Boolean { var valid = true keyRefs.values.forEach { if (!it.isPresent && it.key.left() != "") { LOGGER.warn("Registry '$name' reference at '${it.key.left()}' is not bound to value, expect problems (referenced ${it.references} times)") valid = false } } idRefs.values.forEach { if (!it.isPresent) { LOGGER.warn("Registry '$name' reference with ID '${it.key.right()}' is not bound to value, expect problems (referenced ${it.references} times)") valid = false } } return valid } fun add(key: String, value: T, json: JsonElement, file: IStarboundFile): Entry { require(key != "") { "Adding $name with empty name (empty name is reserved)" } lock.withLock { if (key in keysInternal) { LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: ""})") } val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) }) check(!entry.isBuiltin) { "Trying to redefine builtin entry" } entry.id?.let { idsInternal.remove(it) idRefs[it]?.entry = null } entry.id = null entry.value = value entry.json = json entry.file = file keyRefs[key]?.entry = entry return entry } } fun add(key: String, id: Int, value: T, json: JsonElement, file: IStarboundFile): Entry { require(key != "") { "Adding $name with empty name (empty name is reserved)" } lock.withLock { if (key in keysInternal) { LOGGER.warn("Overwriting $name at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: ""})") } if (id in idsInternal) { LOGGER.warn("Overwriting $name with ID '$id' (new def originate from $file; old def originate from ${idsInternal[id]?.file ?: ""})") } val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) }) check(!entry.isBuiltin) { "Trying to redefine builtin entry" } entry.id?.let { idsInternal.remove(it) idRefs[it]?.entry = null } entry.id = id entry.value = value entry.json = json entry.file = file keyRefs[key]?.entry = entry idRefs[id]?.entry = entry idsInternal[id] = entry return entry } } fun add(key: String, value: T, isBuiltin: Boolean = false): Entry { require(key != "") { "Adding $name with empty name (empty name is reserved)" } lock.withLock { if (key in keysInternal) { LOGGER.warn("Overwriting $name at '$key' (new def originate from ; old def originate from ${keysInternal[key]?.file ?: ""})") } val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) }) check(!entry.isBuiltin || isBuiltin) { "Trying to redefine builtin entry" } entry.id?.let { idsInternal.remove(it) idRefs[it]?.entry = null } entry.id = null entry.value = value entry.json = JsonNull.INSTANCE entry.file = null entry.isBuiltin = isBuiltin keyRefs[key]?.entry = entry return entry } } fun add(key: String, id: Int, value: T, isBuiltin: Boolean = false): Entry { require(key != "") { "Adding $name with empty name (empty name is reserved)" } lock.withLock { if (key in keysInternal) { LOGGER.warn("Overwriting $name at '$key' (new def originate from ; old def originate from ${keysInternal[key]?.file ?: ""})") } if (id in idsInternal) { LOGGER.warn("Overwriting $name with ID '$id' (new def originate from ; old def originate from ${idsInternal[id]?.file ?: ""})") } val entry = keysInternal.computeIfAbsent(key, Object2ObjectFunction { Impl(key, value) }) check(!entry.isBuiltin || isBuiltin) { "Trying to redefine builtin entry" } entry.id?.let { idsInternal.remove(it) idRefs[it]?.entry = null } entry.id = id entry.value = value entry.json = JsonNull.INSTANCE entry.file = null entry.isBuiltin = isBuiltin keyRefs[key]?.entry = entry idRefs[id]?.entry = entry idsInternal[id] = entry return entry } } val emptyRef = ref("") companion object { private val LOGGER = LogManager.getLogger() } }