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())
		}
	}
}