186 lines
4.7 KiB
Kotlin
186 lines
4.7 KiB
Kotlin
package ru.dbotthepony.kstarbound.util
|
||
|
||
import com.google.common.collect.ImmutableList
|
||
import com.google.common.collect.ImmutableMap
|
||
import com.google.common.collect.ImmutableSet
|
||
import com.google.common.collect.Interners
|
||
import com.google.gson.Gson
|
||
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.JsonWriter
|
||
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
|
||
|
||
/**
|
||
* Шаблонизировання строка в стиле Starbound'а
|
||
*
|
||
* Представляет из себя строки вида:
|
||
* * `my.thing`
|
||
* * `frame_<FrameNumber>`
|
||
* * `<frame><effectDirectives>`
|
||
*/
|
||
class SBPattern private constructor(
|
||
val raw: String,
|
||
val params: ImmutableMap<String, String>,
|
||
val pieces: ImmutableList<Piece>,
|
||
val names: ImmutableSet<String>
|
||
) {
|
||
val isPlainString get() = names.isEmpty()
|
||
val value by lazy { resolve { null } }
|
||
|
||
override fun toString(): String {
|
||
return "SBPattern[$raw, $params]"
|
||
}
|
||
|
||
override fun equals(other: Any?): Boolean {
|
||
return other === this || other is SBPattern && other.raw == raw && other.names == names && other.pieces == pieces && other.params == params
|
||
}
|
||
|
||
@Volatile
|
||
private var calculatedHash = false
|
||
@Volatile
|
||
private var hash = 0
|
||
|
||
override fun hashCode(): Int {
|
||
if (!calculatedHash) {
|
||
hash = raw.hashCode().xor(params.hashCode()).rotateLeft(12).and(pieces.hashCode()).rotateRight(8).xor(names.hashCode())
|
||
calculatedHash = true
|
||
}
|
||
|
||
return hash
|
||
}
|
||
|
||
fun resolve(values: (String) -> String?): String? {
|
||
if (names.isEmpty()) {
|
||
return raw
|
||
} else if (pieces.size == 1) {
|
||
return pieces[0].resolve(values, params::get)
|
||
}
|
||
|
||
val buffer = ArrayList<String>(pieces.size)
|
||
|
||
for (piece in pieces) {
|
||
buffer.add(piece.resolve(values, params::get) ?: return null)
|
||
}
|
||
|
||
var count = 0
|
||
for (piece in buffer) count += piece.length
|
||
val builder = StringBuilder(count)
|
||
for (piece in buffer) builder.append(piece)
|
||
return String(builder)
|
||
}
|
||
|
||
fun resolve(values: Map<String, String>): String? {
|
||
return resolve(values::get)
|
||
}
|
||
|
||
fun with(params: Map<String, String>): SBPattern {
|
||
return with(params::get)
|
||
}
|
||
|
||
fun with(params: (String) -> String?): SBPattern {
|
||
if (names.isEmpty())
|
||
return this
|
||
|
||
val map = Object2ObjectArrayMap<String, String>()
|
||
map.putAll(this.params)
|
||
var any = false
|
||
|
||
for (name in names) {
|
||
val get = params.invoke(name)
|
||
|
||
if (get != null && get != map[name]) {
|
||
map[name] = get
|
||
any = true
|
||
}
|
||
}
|
||
|
||
if (!any)
|
||
return this
|
||
|
||
return SBPattern(raw, ImmutableMap.copyOf(map), pieces, names)
|
||
}
|
||
|
||
data class Piece(val name: String? = null, val contents: String? = null) {
|
||
init {
|
||
check(name != null || contents != null) { "Both name and contents are null" }
|
||
check(!(name != null && contents != null)) { "Both name and contents are not null" }
|
||
}
|
||
|
||
fun resolve(map0: (String) -> String?, map1: (String) -> String?): String? {
|
||
return contents ?: map0.invoke(name!!) ?: map1.invoke(name!!)
|
||
}
|
||
}
|
||
|
||
companion object : TypeAdapterFactory {
|
||
private val interner = Interners.newWeakInterner<SBPattern>()
|
||
|
||
@JvmField
|
||
val EMPTY = raw("")
|
||
|
||
@JvmField
|
||
val FRAME = of("<frame>")
|
||
|
||
override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
|
||
if (type.rawType == SBPattern::class.java) {
|
||
return object : TypeAdapter<SBPattern>() {
|
||
private val strings = gson.getAdapter(String::class.java)
|
||
|
||
override fun write(out: JsonWriter, value: SBPattern?) {
|
||
strings.write(out, value?.raw)
|
||
}
|
||
|
||
override fun read(`in`: JsonReader): SBPattern? {
|
||
return of(strings.read(`in`) ?: return null)
|
||
}
|
||
} as TypeAdapter<T>
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
@JvmStatic
|
||
fun of(raw: String): SBPattern {
|
||
if (raw == "")
|
||
return EMPTY
|
||
|
||
val pieces = ImmutableList.Builder<Piece>()
|
||
var i = 0
|
||
|
||
while (i < raw.length) {
|
||
val open = raw.indexOf('<', startIndex = i)
|
||
|
||
if (open == -1) {
|
||
if (i == 0)
|
||
pieces.add(Piece(contents = raw))
|
||
else
|
||
pieces.add(Piece(contents = raw.substring(i)))
|
||
|
||
break
|
||
} else {
|
||
val closing = raw.indexOf('>', startIndex = open + 1)
|
||
|
||
if (closing == -1) {
|
||
throw IllegalArgumentException("Malformed pattern string: $raw")
|
||
}
|
||
|
||
pieces.add(Piece(name = raw.substring(open + 1, closing)))
|
||
i = closing + 1
|
||
}
|
||
}
|
||
|
||
val built = pieces.build()
|
||
return interner.intern(SBPattern(raw, pieces = built, params = ImmutableMap.of(), names = built.stream().map { it.name }.filter { it != null }.collect(ImmutableSet.toImmutableSet())))
|
||
}
|
||
|
||
@JvmStatic
|
||
fun raw(raw: String): SBPattern {
|
||
if (raw == "")
|
||
return EMPTY
|
||
|
||
return SBPattern(raw, ImmutableMap.of(), ImmutableList.of(), ImmutableSet.of())
|
||
}
|
||
}
|
||
}
|