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_<FrameNumber>`
 * * `<frame><effectDirectives>`
 */
class SBPattern private constructor(
	val raw: String,
	private val params: Array<String?>,
	val pieces: ImmutableList<Piece>,
	val namesSet: ImmutableSet<String>,
	val namesList: ImmutableList<String>,
) {
	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 `<tags>` 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<String>(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<String>(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, String>): String? {
		return resolve(values::get)
	}

	fun with(params: Map<String, String>): 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<SBPattern> = Starbound.interner(5)

		@JvmField
		val EMPTY = raw("")

		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.sbIntern()))
					else
						pieces.add(Piece(contents = raw.substring(i).sbIntern()))

					break
				} else {
					if (open != i) {
						pieces.add(Piece(contents = raw.substring(i, open).sbIntern()))
					}

					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).sbIntern()))
					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<String>,
				namesList = ImmutableList.copyOf(names)
			)

			return interner.intern(result)
		}

		private val emptyParams = arrayOfNulls<String>(0)

		@JvmStatic
		fun raw(raw: String): SBPattern {
			if (raw == "")
				return EMPTY

			return SBPattern(raw, emptyParams, ImmutableList.of(), ImmutableSet.of(), ImmutableList.of())
		}
	}
}
