package ru.dbotthepony.kstarbound.world.terrain

import com.google.gson.Gson
import com.google.gson.JsonElement
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.contains
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.defs.world.TerrainSelectorParameters

private inline fun <reified D : Any, T : AbstractTerrainSelector<D>> data(noinline factory: (D, TerrainSelectorParameters) -> T): Data<D, T> {
	return Data(TypeToken.get(D::class.java), factory)
}

private data class Data<D : Any, out T : AbstractTerrainSelector<D>>(val token: TypeToken<D>, val factory: (D, TerrainSelectorParameters) -> T) {
	val adapter: TypeAdapter<D> by lazy { Starbound.gson.getAdapter(token) }
}

enum class TerrainSelectorType(val jsonName: String, private val data: Data<*, *>) {
	CONSTANT("constant", data(::ConstantTerrainSelector)),
	DISPLACEMENT("displacement", data(::DisplacementTerrainSelector)),
	FLAT_SURFACE("flatSurface", data(::FlatSurfaceTerrainSelector)),
	ISLAND_SURFACE("islandSurface", data(::IslandSurfaceTerrainSelector)),
	MAX("max", data(ComparingTerrainSelector::Max)),
	MIN("min", data(ComparingTerrainSelector::Min)),
	MIN_MAX("minmax", data(ComparingTerrainSelector::MinMax)),
	PERLIN("perlin", data(::PerlinTerrainSelector)),
	KARST_CAVE("karstcave", data(::KarstCaveTerrainSelector)),
	MIX("mix", data(::MixTerrainSelector)),
	ROTATE("rotate", data(::RotateTerrainSelector)),
	RIDGE_BLOCKS("ridgeblocks", data(::RidgeBlocksTerrainSelector)),
	WORM_CAVE("wormcave", data(::WormCaveTerrainSelector)),
	;

	private val lowercase = jsonName.lowercase()

	data class Factory<D : Any, out T : AbstractTerrainSelector<D>>(private val data: D, private val factory: (D, TerrainSelectorParameters) -> T) {
		fun create(parameters: TerrainSelectorParameters): T {
			return factory(data, parameters)
		}
	}

	companion object : TypeAdapter<AbstractTerrainSelector<*>>(), TypeAdapterFactory {
		override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
			if (AbstractTerrainSelector::class.java.isAssignableFrom(type.rawType)) {
				return this as TypeAdapter<T>
			}

			return null
		}

		override fun write(out: JsonWriter, value: AbstractTerrainSelector<*>?) {
			if (value == null)
				out.nullValue()
			else
				out.value(value.toJson())
		}

		fun named(name: String, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> {
			return Registries.terrainSelectors.getOrThrow(name).value.create(parameters)
		}

		override fun read(`in`: JsonReader): AbstractTerrainSelector<*>? {
			if (`in`.consumeNull())
				return null

			return load(Starbound.ELEMENTS_ADAPTER.objects.read(`in`))
		}

		fun factory(json: JsonObject, isSerializedForm: Boolean, type: TerrainSelectorType? = null): Factory<*, *> {
			val type = type?.jsonName ?: json["type"]?.asString?.lowercase() ?: throw JsonSyntaxException("Missing 'type' element of terrain json")

			if (isSerializedForm) {
				val config = json["config"]?.asJsonObject ?: throw JsonSyntaxException("Missing 'config' element of terrain json")

				for (value in entries) {
					if (value.lowercase == type) {
						return Factory(value.data.adapter.fromJsonTree(config), value.data.factory as ((Any, TerrainSelectorParameters) -> AbstractTerrainSelector<Any>))
					}
				}
			} else {
				for (value in entries) {
					if (value.lowercase == type) {
						return Factory(value.data.adapter.fromJsonTree(json), value.data.factory as ((Any, TerrainSelectorParameters) -> AbstractTerrainSelector<Any>))
					}
				}
			}

			throw IllegalArgumentException("Unknown terrain selector type $type")
		}

		fun create(json: JsonElement, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> {
			if (json is JsonObject) {
				if ("name" in json) {
					return named(json["name"].asString, parameters)
				} else {
					return factory(json, false).create(parameters)
				}
			} else {
				return named(json.asString, parameters)
			}
		}

		fun load(json: JsonObject): AbstractTerrainSelector<*> {
			return factory(json, true).create(Starbound.gson.fromJson(json["parameters"] ?: throw JsonSyntaxException("Missing 'parameters' element of terrain json"), TerrainSelectorParameters::class.java))
		}

		fun load(json: JsonObject, parameters: TerrainSelectorParameters): AbstractTerrainSelector<*> {
			return factory(json, true).create(parameters)
		}
	}
}
