package ru.dbotthepony.kstarbound.util import com.github.benmanes.caffeine.cache.Interner import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableSet 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 ru.dbotthepony.kstarbound.Starbound /** * Шаблонизировання строка в стиле Starbound'а * * Представляет из себя строки вида: * * `my.thing` * * `frame_` * * `` */ class SBPattern private constructor( val raw: String, private val params: Array, val pieces: ImmutableList, val namesSet: ImmutableSet, val namesList: ImmutableList, ) { fun getParam(name: String): String? { val index = namesList.indexOf(name) if (index == -1) return null return params[index] } val isPlainString get() = namesSet.isEmpty() val value by lazy { resolve { null } } override fun toString(): String { return "SBPattern[$raw, ${params.withIndex().joinToString("; ") { "${namesList[it.index]}=${it.value}" }}]" } override fun equals(other: Any?): Boolean { return other === this || other is SBPattern && other.raw == raw && other.namesSet == namesSet && other.pieces == pieces && other.params.contentEquals(params) } @Volatile private var calculatedHash = false @Volatile private var hash = 0 override fun hashCode(): Int { if (!calculatedHash) { hash = raw.hashCode().xor(params.contentHashCode()).rotateLeft(12).and(pieces.hashCode()).rotateRight(8).xor(namesSet.hashCode()) calculatedHash = true } return hash } /** * Different from regular resolve that this always returns non-null string, * even if we didn't replace all tags (not replaced tags appear as `` or as [defaultValue], * depending on [replaceWithDefault]) */ fun resolveOrSkip(values: (String) -> String?, replaceWithDefault: Boolean = false, defaultValue: String = ""): String { if (namesSet.isEmpty()) { return raw } else if (pieces.size == 1) { val resolve = pieces[0].resolve(values, this::getParam) ?: if (replaceWithDefault) { return defaultValue } else { return raw } return resolve } val buffer = ArrayList(pieces.size) for (piece in pieces) { var resolve = piece.resolve(values, this::getParam) if (resolve == null) { if (replaceWithDefault) { resolve = defaultValue } else { resolve = "<${piece.name!!}>" } } buffer.add(resolve) } 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: (String) -> String?): String? { if (namesSet.isEmpty()) { return raw } else if (pieces.size == 1) { return pieces[0].resolve(values, this::getParam) } val buffer = ArrayList(pieces.size) for (piece in pieces) { buffer.add(piece.resolve(values, this::getParam) ?: 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? { return resolve(values::get) } fun with(params: Map): SBPattern { return with(params::get) } fun with(params: (String) -> String?): SBPattern { when (namesList.size) { 0 -> return this 1 -> { val get = params.invoke(namesList[0]) if (get == this.params[0]) { return this } return SBPattern(raw, arrayOf(get), pieces, namesSet, namesList) } 2 -> { val get0 = params.invoke(namesList[0]) val get1 = params.invoke(namesList[1]) if (get0 == this.params[0] && get1 == this.params[1]) { return this } return SBPattern(raw, arrayOf(get0, get1), pieces, namesSet, namesList) } 3 -> { val get0 = params.invoke(namesList[0]) val get1 = params.invoke(namesList[1]) val get2 = params.invoke(namesList[2]) if (get0 == this.params[0] && get1 == this.params[1] && get2 == this.params[2]) { return this } return SBPattern(raw, arrayOf(get0, get1, get2), pieces, namesSet, namesList) } else -> { val newArray = this.params.copyOf() var any = false for ((i, name) in namesList.withIndex()) { val value = this.params[i] val get = params.invoke(name) if (value != get) { newArray[i] = value any = true } } if (!any) return this return SBPattern(raw, newArray, pieces, namesSet, namesList) } } } 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" } } inline fun resolve(map0: (String) -> String?, map1: (String) -> String?): String? { return contents ?: map0.invoke(name!!) ?: map1.invoke(name!!) } } companion object : TypeAdapterFactory { private val interner: Interner = Starbound.interner(5) @JvmField val EMPTY = raw("") override fun create(gson: Gson, type: TypeToken): TypeAdapter? { if (type.rawType == SBPattern::class.java) { return object : TypeAdapter() { 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 } return null } @JvmStatic fun of(raw: String): SBPattern { if (raw == "") return EMPTY val pieces = ImmutableList.Builder() 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 { if (open != i) { pieces.add(Piece(contents = raw.substring(i, open))) } val closing = raw.indexOf('>', startIndex = open + 1) if (closing == -1) { throw IllegalArgumentException("Malformed pattern string: $raw") } pieces.add(Piece(name = Starbound.STRINGS.intern(raw.substring(open + 1, closing)))) i = closing + 1 } } val built = pieces.build() val names = built.stream().map { it.name }.filter { it != null }.collect(ImmutableSet.toImmutableSet()) val result = SBPattern( raw, pieces = built, params = arrayOfNulls(names.size), namesSet = names as ImmutableSet, namesList = ImmutableList.copyOf(names) ) return interner.intern(result) } private val emptyParams = arrayOfNulls(0) @JvmStatic fun raw(raw: String): SBPattern { if (raw == "") return EMPTY return SBPattern(raw, emptyParams, ImmutableList.of(), ImmutableSet.of(), ImmutableList.of()) } } }