package ru.dbotthepony.kstarbound import com.google.gson.Gson import com.google.gson.JsonElement import com.google.gson.JsonNull import com.google.gson.JsonObject import com.google.gson.JsonSyntaxException import com.google.gson.TypeAdapter import com.google.gson.TypeAdapterFactory import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter 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 it.unimi.dsi.fastutil.objects.Object2ObjectMap import it.unimi.dsi.fastutil.objects.Object2ObjectMaps import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import org.apache.logging.log4j.LogManager import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.json.consumeNull import java.lang.reflect.ParameterizedType import java.util.concurrent.locks.ReentrantLock import java.util.function.Supplier import kotlin.collections.contains import kotlin.collections.set import kotlin.concurrent.withLock import ru.dbotthepony.kstarbound.util.traverseJsonPath import java.util.concurrent.ConcurrentLinkedQueue inline fun Registry.adapter(): TypeAdapterFactory { return object : TypeAdapterFactory { override fun create(gson: Gson, type: TypeToken): TypeAdapter? { val subtype = type.type as? ParameterizedType ?: return null if (subtype.actualTypeArguments.size != 1 || subtype.actualTypeArguments[0] != S::class.java) return null return when (type.rawType) { Registry.Entry::class.java -> { object : TypeAdapter>() { override fun write(out: JsonWriter, value: Registry.Entry?) { if (value != null) { out.value(value.key) } else { out.nullValue() } } override fun read(`in`: JsonReader): Registry.Entry? { if (`in`.consumeNull()) { return null } else if (`in`.peek() == JsonToken.STRING) { return this@adapter[`in`.nextString()] } else if (`in`.peek() == JsonToken.NUMBER) { return this@adapter[`in`.nextInt()] } else { throw JsonSyntaxException("Expected registry key or registry id, got ${`in`.peek()}") } } } } Registry.Ref::class.java -> { object : TypeAdapter>() { override fun write(out: JsonWriter, value: Registry.Ref?) { if (value != null) { value.key.map(out::value, out::value) } else { out.nullValue() } } override fun read(`in`: JsonReader): Registry.Ref? { if (`in`.consumeNull()) { return null } else if (`in`.peek() == JsonToken.STRING) { return this@adapter.ref(`in`.nextString()) } else if (`in`.peek() == JsonToken.NUMBER) { return this@adapter.ref(`in`.nextInt()) } else { throw JsonSyntaxException("Expected registry key or registry id, got ${`in`.peek()}") } } } } else -> null } as TypeAdapter? } } } class Registry(val name: String) { private val keysInternal = Object2ObjectOpenHashMap() private val idsInternal = Int2ObjectOpenHashMap() private val keyRefs = Object2ObjectOpenHashMap() 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() { var next = backlog.poll() while (next != null) { next.run() next = backlog.poll() } } private val lock = ReentrantLock() val keys: Object2ObjectMap> = Object2ObjectMaps.unmodifiable(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 value: T? get() = entry?.value fun traverseJsonPath(path: String): JsonElement? { return traverseJsonPath(path, entry?.json ?: return null) } 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 abstract val isBuiltin: Boolean fun traverseJsonPath(path: String): JsonElement? { return traverseJsonPath(path, json) } 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 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=${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 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 any = true keyRefs.values.forEach { if (!it.isPresent) { LOGGER.warn("Registry '$name' reference at '${it.key.left()}' is not bound to value, expect problems (referenced ${it.references} times)") any = 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)") any = false } } return any } fun add(key: String, value: T, json: JsonElement, file: IStarboundFile): Entry { lock.withLock { if (key in keysInternal) { LOGGER.warn("Overwriting Registry entry 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 { lock.withLock { if (key in keysInternal) { LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from $file; old def originate from ${keysInternal[key]?.file ?: ""})") } if (id in idsInternal) { LOGGER.warn("Overwriting Registry entry 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 { lock.withLock { if (key in keysInternal) { LOGGER.warn("Overwriting Registry entry 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 { lock.withLock { if (key in keysInternal) { LOGGER.warn("Overwriting Registry entry at '$key' (new def originate from ; old def originate from ${keysInternal[key]?.file ?: ""})") } if (id in idsInternal) { LOGGER.warn("Overwriting Registry entry 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 } } companion object { private val LOGGER = LogManager.getLogger() } }