package ru.dbotthepony.kstarbound.defs import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.TypeAdapter import com.google.gson.reflect.TypeToken import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap import ru.dbotthepony.kommons.util.Either import ru.dbotthepony.kstarbound.Starbound import ru.dbotthepony.kstarbound.util.AssetPathStack import ru.dbotthepony.kommons.gson.set import java.util.function.Consumer import java.util.function.Function import java.util.function.Supplier import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty import kotlin.reflect.javaType /** * Base class for instances which can be mutated by altering underlying JSON */ abstract class JsonDriven(val path: String) { private val delegates = ArrayList>() private val delegatesMap = Object2ObjectOpenHashMap>>() private val lazies = ArrayList>() private val namedLazies = Object2ObjectOpenHashMap>>() protected val properties = JsonObject() /** * [JsonObject]s which define behavior of properties */ protected abstract fun defs(): Collection protected open fun invalidate() { delegates.forEach { it.invalidate() } lazies.forEach { it.invalidate() } } protected open fun invalidate(name: String) { delegatesMap[name]?.forEach { it.invalidate() } namedLazies[name]?.forEach { it.invalidate() } lazies.forEach { it.invalidate() } } inner class LazyData(names: Iterable = listOf(), private val initializer: () -> T) : Lazy { constructor(initializer: () -> T) : this(listOf(), initializer) init { for (name in names) { namedLazies.computeIfAbsent(name, Function { ArrayList() }).add(this) } } private var _value: Any? = mark override val value: T get() { var value = _value if (value !== mark) { return value as T } value = initializer.invoke() _value = value return value } override fun isInitialized(): Boolean { return _value !== mark } fun invalidate() { _value = mark } } inner class Property( name: String? = null, val default: Either, JsonElement>? = null, private var adapter: TypeAdapter? = null, ) : Supplier, Consumer, ReadWriteProperty { constructor(name: String, default: T, adapter: TypeAdapter? = null) : this(name, Either.left(Supplier { default }), adapter) constructor(name: String, default: Supplier, adapter: TypeAdapter? = null) : this(name, Either.left(default), adapter) constructor(name: String, default: JsonElement, adapter: TypeAdapter? = null) : this(name, Either.right(default), adapter) constructor(default: T, adapter: TypeAdapter? = null) : this(null, Either.left(Supplier { default }), adapter) constructor(default: Supplier, adapter: TypeAdapter? = null) : this(null, Either.left(default), adapter) constructor(default: JsonElement, adapter: TypeAdapter? = null) : this(null, Either.right(default), adapter) var name: String? = name private set(value) { if (field != null) throw IllegalStateException() field = value delegatesMap.computeIfAbsent(value, Function { ArrayList() }).add(this) } init { delegates.add(this) if (name != null) delegatesMap.computeIfAbsent(name, Function { ArrayList() }).add(this) } private var value: Supplier = never as Supplier private fun compute(): T { val value = dataValue(checkNotNull(name)) if (value == null) { if (default == null) { throw NoSuchElementException("No json value present at '$name', and no default value was provided") } else if (default.isLeft) { return default.left().get() } else { AssetPathStack.block(path) { return adapter!!.fromJsonTree(default.right()) } } } else { AssetPathStack.block(path) { return adapter!!.fromJsonTree(value) } } } fun invalidate() { value = Supplier { val new = compute() this.value = Supplier { new } new } } init { invalidate() } override fun get(): T { return value.get() } override fun accept(t: T) { AssetPathStack.block(path) { properties[checkNotNull(name)] = adapter!!.toJsonTree(t) } // value = Supplier { t } invalidate(name!!) } @OptIn(ExperimentalStdlibApi::class) override fun getValue(thisRef: Any?, property: KProperty<*>): T { if (adapter == null) { adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter } if (name == null) { name = property.name } return value.get() } @OptIn(ExperimentalStdlibApi::class) override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { if (adapter == null) { adapter = Starbound.gson.getAdapter(TypeToken.get(property.returnType.javaType)) as TypeAdapter } if (name == null) { name = property.name } return accept(value) } } fun dataValue(name: String, alwaysCopy: Boolean = false): JsonElement? { val defs = defs() var value: JsonElement? if (defs.isEmpty()) { value = properties[name]?.let { if (alwaysCopy) it.deepCopy() else it } } else { val itr = defs.iterator() var isCopy = false value = properties[name] while ((value == null || value is JsonObject) && itr.hasNext()) { val next = itr.next()[name] if (value is JsonObject) { if (next !is JsonObject) continue value = mergeNoCopy(if (isCopy) value else value.deepCopy(), next) isCopy = true } else { value = next } } } return value } fun hasDataValue(name: String): Boolean { if (properties[name] != null) return true return defs().any { it[name] != null } } companion object { private val mark = Any() private val never = Supplier { throw NoSuchElementException() } @JvmStatic fun mergeNoCopy(a: JsonObject, b: JsonObject): JsonObject { for ((k, v) in b.entrySet()) { val existing = a[k] if (existing is JsonObject && v is JsonObject) { a[k] = mergeNoCopy(existing, v) } else if (existing !is JsonObject) { a[k] = v.deepCopy() } } return a } } }