package ru.dbotthepony.kstarbound.json.builder

import com.google.gson.Gson
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
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.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kstarbound.Starbound

inline fun <reified E : Any, reified C : Any> DispatchingAdapter(
	key: String,
	noinline value2type: C.() -> E,
	noinline type2value: E.() -> TypeToken<out C>,
	elements: Collection<E>,
): DispatchingAdapter<E, C> {
	return DispatchingAdapter(key, value2type, type2value, elements, E::class.java, C::class.java)
}

class DispatchingAdapter<TYPE : Any, ELEMENT : Any>(
	val key: String,
	val value2type: ELEMENT.() -> TYPE,
	val type2value: TYPE.() -> TypeToken<out ELEMENT>,
	val types: Collection<TYPE>,
	val typeClass: Class<TYPE>,
	val baseValueClass: Class<ELEMENT>
) : TypeAdapterFactory {
	override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
		if (type.rawType == baseValueClass) {
			return Impl(gson) as TypeAdapter<T>
		}

		return null
	}

	private inner class Impl(gson: Gson) : TypeAdapter<ELEMENT>() {
		private val typeAdapter = gson.getAdapter(typeClass)
		private val adapters = types.associateWith { gson.getAdapter(type2value.invoke(it)) } as Map<TYPE, TypeAdapter<ELEMENT>>

		override fun write(out: JsonWriter, value: ELEMENT?) {
			if (value == null)
				out.nullValue()
			else {
				val type = value2type(value)
				val adapter = adapters[type] ?: throw JsonSyntaxException("Unknown type $type")

				out.beginObject()
				out.name(key)
				typeAdapter.write(out, type)

				when (val result = adapter.toJsonTree(value)) {
					JsonNull.INSTANCE -> {} // do nothing, probably singleton value

					// TODO: this is a considerable bottleneck, need proxied json writer which will hook right before top-most endObject()
					is JsonObject -> {
						for ((k, v) in result.entrySet()) {
							out.name(k)
							out.value(v)
						}
					}

					else -> throw JsonSyntaxException("Expected JSON Object or JSON NULL, got ${result::class.java}")
				}

				out.endObject()
			}
		}

		override fun read(`in`: JsonReader): ELEMENT? {
			if (`in`.consumeNull())
				return null

			val read = Starbound.ELEMENTS_ADAPTER.objects.read(`in`)
			val type = typeAdapter.fromJsonTree(read[key] ?: throw JsonSyntaxException("Missing '$key'"))
			val adapter = adapters[type] ?: throw JsonSyntaxException("Unknown type $type (${read[key]})")
			return adapter.fromJsonTree(read)
		}
	}
}
