package ru.dbotthepony.kstarbound.defs.world

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
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.collect.filterNotNull
import ru.dbotthepony.kommons.gson.consumeNull
import ru.dbotthepony.kommons.gson.stream
import ru.dbotthepony.kommons.gson.value
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registries
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.Starbound
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.tile.BuiltinMetaMaterials
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat
import ru.dbotthepony.kstarbound.json.listAdapter
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.staticRandomDouble
import ru.dbotthepony.kstarbound.util.random.staticRandomInt
import java.util.stream.Stream

@JsonFactory
data class BiomePlaceables(
	val grassMod: NativeLegacy.TileMod = NativeLegacy.TileMod(BuiltinMetaMaterials.EMPTY_MOD),
	val ceilingGrassMod: NativeLegacy.TileMod = NativeLegacy.TileMod(BuiltinMetaMaterials.EMPTY_MOD),
	val grassModDensity: Double = 0.0,
	val ceilingGrassModDensity: Double = 0.0,
	val itemDistributions: ImmutableList<DistributionItem> = ImmutableList.of(),
) {
	fun firstTreeVariant(): TreeVariant? {
		return itemDistributions.stream()
			.flatMap { it.data.itemStream() }
			.map { it as? Tree }
			.filterNotNull()
			.flatMap { it.trees.stream() }
			.findAny()
			.orElse(null)
	}

	// ----------- ITEMS
	@JsonFactory
	data class DistributionItem(
		val priority: Double = 0.0,
		val variants: Int = 1,
		val mode: BiomePlaceablesDefinition.Placement = BiomePlaceablesDefinition.Placement.FLOOR,
		@JsonFlat
		val data: DistributionData,
	) {
		fun itemToPlace(x: Int, y: Int): Placement? {
			return data.itemToPlace(x, y, priority)
		}
	}

	data class Placement(val item: Item, val position: Vector2i, val priority: Double)

	abstract class Item {
		abstract val type: BiomePlacementItemType

		abstract fun toJson(): JsonElement

		companion object : TypeAdapterFactory {
			override fun <T : Any> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
				if (Item::class.java.isAssignableFrom(type.rawType)) {
					return object : TypeAdapter<Item>() {
						private val grassVariant = gson.getAdapter(GrassVariant::class.java)
						private val bushVariant = gson.getAdapter(BushVariant::class.java)
						private val trees = gson.listAdapter<TreeVariant>()
						private val objects = gson.getAdapter(PoolTypeToken)

						override fun write(out: JsonWriter, value: Item?) {
							if (value == null)
								out.nullValue()
							else {
								out.beginArray()

								when (value.type) {
									BiomePlacementItemType.MICRO_DUNGEON -> out.value("microDungeon")
									BiomePlacementItemType.TREASURE_BOX_SET -> out.value("treasureBoxSet")
									BiomePlacementItemType.GRASS -> out.value("grass")
									BiomePlacementItemType.BUSH -> out.value("bush")
									BiomePlacementItemType.TREE -> out.value("treePair")
									BiomePlacementItemType.OBJECT -> out.value("objectPool")
								}

								out.value(value.toJson())
								out.endArray()
							}
						}

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

							`in`.beginArray()

							// sucks that this has to be done manually
							// Also I salute to whoever decided to give different names
							// and different comparison rules for data in *.biome files
							// and world storage data at Chucklefish.
							// Truly our hero here.
							val obj = when (val type = `in`.nextString()) {
								"treasureBoxSet" -> TreasureBox(`in`.nextString())
								"microDungeon" -> MicroDungeon(Starbound.ELEMENTS_ADAPTER.arrays.read(`in`).stream().map { Registries.dungeons.ref(it.asString) }.collect(ImmutableSet.toImmutableSet()))
								"grass" -> Grass(grassVariant.read(`in`))
								"bush" -> Bush(bushVariant.read(`in`))
								"treePair" -> Tree(trees.read(`in`))
								"objectPool" -> Object(objects.read(`in`))
								else -> throw JsonSyntaxException("Unknown biome placement item $type")
							}

							`in`.endArray()
							return obj
						}
					} as TypeAdapter<T>
				}

				return null
			}
		}
	}

	data class MicroDungeon(val microdungeons: ImmutableSet<Registry.Ref<DungeonDefinition>> = ImmutableSet.of()) : Item() {
		override val type: BiomePlacementItemType
			get() = BiomePlacementItemType.MICRO_DUNGEON

		override fun toJson(): JsonElement {
			return JsonArray().also { j ->
				microdungeons.forEach { j.add(JsonPrimitive(it.key.left())) }
			}
		}
	}

	data class TreasureBox(val pool: String) : Item() {
		override val type: BiomePlacementItemType
			get() = BiomePlacementItemType.TREASURE_BOX_SET

		override fun toJson(): JsonElement {
			return JsonPrimitive(pool)
		}
	}

	data class Grass(val value: GrassVariant) : Item() {
		override val type: BiomePlacementItemType
			get() = BiomePlacementItemType.GRASS

		override fun toJson(): JsonElement {
			return Starbound.gson.toJsonTree(value)
		}
	}

	data class Bush(val value: BushVariant) : Item() {
		override val type: BiomePlacementItemType
			get() = BiomePlacementItemType.BUSH

		override fun toJson(): JsonElement {
			return Starbound.gson.toJsonTree(value)
		}
	}

	data class Tree(val trees: ImmutableList<TreeVariant>) : Item() {
		override val type: BiomePlacementItemType
			get() = BiomePlacementItemType.TREE

		override fun toJson(): JsonElement {
			return Starbound.gson.toJsonTree(trees, TypeToken.getParameterized(
				ImmutableList::class.java,
				TreeVariant::class.java
			).type)
		}
	}

	private object PoolTypeToken : TypeToken<WeightedList<Pair<String, JsonElement>>>()

	// This structure sucks, but at least it allows unique parameters per
	// each object (lmao, whos gonna write world json by hand anyway????
	// considering this is world generation data.)
	data class Object(val pool: WeightedList<Pair<String, JsonElement>>) : Item() {
		override val type: BiomePlacementItemType
			get() = BiomePlacementItemType.OBJECT

		override fun toJson(): JsonElement {
			return Starbound.gson.toJsonTree(pool, PoolTypeToken.type)
		}
	}

	// ТЕПЕРЬ УГАДАЙ
	// МОЙ
	// ПОЛ.
	// БЛЯДЬ
	@JsonFactory
	data class DistributionData(
		val distribution: BiomePlacementDistributionType,
		val blockSeed: Long = 0L,
		val blockProbability: Double = 0.0,
		val randomItems: ImmutableList<Item> = ImmutableList.of(),
		val modulus: Int = 0,
		val modulusOffset: Int = 0,
		val densityFunction: AbstractPerlinNoise = AbstractPerlinNoise.of(PerlinNoiseParameters()),
		val modulusDistortion: AbstractPerlinNoise = AbstractPerlinNoise.of(PerlinNoiseParameters()),
		val weightedItems: ImmutableList<Pair<Item, AbstractPerlinNoise>> = ImmutableList.of(),
	) {
		fun itemStream(): Stream<Item> {
			if (distribution == BiomePlacementDistributionType.RANDOM) {
				return randomItems.stream()
			} else {
				return weightedItems.stream().map { it.first }
			}
		}

		fun itemToPlace(x: Int, y: Int, priority: Double): Placement? {
			if (distribution == BiomePlacementDistributionType.RANDOM) {
				if (randomItems.isEmpty())
					return null // whut

				if (staticRandomDouble(x, y, blockSeed) <= blockProbability) {
					return Placement(randomItems[staticRandomInt(0, randomItems.size, x, y, blockSeed)], Vector2i(x, y), priority)
				}
			} else {
				if (weightedItems.isEmpty())
					return null // whut

				if (densityFunction[x.toDouble(), y.toDouble()] > 0.0 && (x + modulusOffset + modulusDistortion[x.toDouble(), y.toDouble()]).toInt() % modulus == 0) {
					var maxWeight = Double.NEGATIVE_INFINITY
					var choosen: Item? = null

					for ((item, distrb) in weightedItems) {
						val weight = distrb[x.toDouble(), y.toDouble()]

						if (weight > maxWeight) {
							maxWeight = weight
							choosen = item
						}
					}

					if (choosen != null) {
						return Placement(choosen, Vector2i(x, y), priority)
					}
				}
			}

			return null
		}
	}
}