package ru.dbotthepony.kstarbound.defs.world

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import ru.dbotthepony.kommons.collect.filterNotNull
import ru.dbotthepony.kommons.vector.Vector2i
import ru.dbotthepony.kstarbound.Registry
import ru.dbotthepony.kstarbound.collect.WeightedList
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.PerlinNoiseParameters
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.json.NativeLegacy
import ru.dbotthepony.kstarbound.json.builder.IStringSerializable
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.json.builder.JsonFlat
import ru.dbotthepony.kstarbound.util.random.AbstractPerlinNoise
import ru.dbotthepony.kstarbound.util.random.nextRange
import ru.dbotthepony.kstarbound.util.random.random
import java.util.stream.IntStream

@JsonFactory
data class BiomePlaceablesDefinition(
	val grassMod: ImmutableList<Registry.Ref<TileModifierDefinition>> = ImmutableList.of(),
	val grassModDensity: Double = 0.0,
	val ceilingGrassMod: ImmutableList<Registry.Ref<TileModifierDefinition>> = ImmutableList.of(),
	val ceilingGrassModDensity: Double = 0.0,
	val items: ImmutableList<DistributionItem> = ImmutableList.of(),
) {
	enum class Placement(override val jsonName: String) : IStringSerializable {
		FLOOR("floor"), CEILING("ceiling"), BACKGROUND("background"), OCEAN("ocean");
	}

	// ----------- ITEMS
	@JsonFactory
	data class DistributionItem(
		val priority: Double = 0.0,
		val variants: Int = 1,
		val mode: Placement = Placement.FLOOR,
		val distribution: AssetReference<DistributionData>,
		@JsonFlat
		val data: DistributionItemData,
	) {
		init {
			checkNotNull(distribution.value) { "Distribution data is missing" }
		}

		fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.DistributionItem {
			return BiomePlaceables.DistributionItem(
				priority = priority,
				variants = variants,
				mode = mode,
				data = distribution.value!!.create(this, biome),
			)
		}
	}

	abstract class DistributionItemData {
		abstract val type: BiomePlacementItemType
		abstract fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item
	}

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

		override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item {
			return BiomePlaceables.MicroDungeon(microdungeons)
		}
	}

	@JsonFactory
	data class TreasureBox(val treasureBoxSets: ImmutableSet<String> = ImmutableSet.of()) : DistributionItemData() {
		override val type: BiomePlacementItemType
			get() = BiomePlacementItemType.MICRO_DUNGEON

		override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item {
			return BiomePlaceables.TreasureBox(treasureBoxSets.random(biome.random))
		}
	}

	@JsonFactory
	data class Grass(val grasses: ImmutableSet<Registry.Ref<GrassVariant.Data>> = ImmutableSet.of()) : DistributionItemData() {
		override val type: BiomePlacementItemType
			get() = BiomePlacementItemType.GRASS

		override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item {
			val valid = grasses.stream().map { it.entry }.filterNotNull().toList()

			if (valid.isEmpty()) {
				throw NoSuchElementException("None of grass variants are valid (candidates: $grasses)")
			}

			return BiomePlaceables.Grass(GrassVariant.Companion.create(valid.random(biome.random), biome.hueShift))
		}
	}

	@JsonFactory
	data class Tree(
		val treeStemList: ImmutableList<Registry.Ref<TreeVariant.StemData>> = ImmutableList.of(),
		val treeFoliageList: ImmutableList<Registry.Ref<TreeVariant.FoliageData>> = ImmutableList.of(),
		val treeStemHueShiftMax: Double = 0.0,
		val treeFoliageHueShiftMax: Double = 0.0,
		val variantsRange: Vector2i = Vector2i(1, 1),
		val subVariantsRange: Vector2i = Vector2i(2, 2),
		val sameStemHueShift: Boolean = true,
		val sameFoliageHueShift: Boolean = false,
	) : DistributionItemData() {
		override val type: BiomePlacementItemType
			get() = BiomePlacementItemType.GRASS

		override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item {
			val validTreeStems = treeStemList.stream().map { it.entry }.filterNotNull().toList()
			val validFoliage = treeFoliageList.stream().filter { it.key.left().isBlank() || it.entry != null }.toList()

			if (validTreeStems.isEmpty()) {
				throw NoSuchElementException("None of tree stems variants are valid (candidates: $treeStemList)")
			}

			if (validFoliage.isEmpty()) {
				throw NoSuchElementException("None of tree foliage variants are valid (candidates: $treeFoliageList)")
			}

			val pairs = ArrayList<Pair<Registry.Entry<TreeVariant.StemData>, Registry.Entry<TreeVariant.FoliageData>?>>()

			for (stem in validTreeStems) {
				for (foliage in validFoliage) {
					if (foliage.entry == null || stem.value.shape == foliage.value!!.shape) {
						pairs.add(stem to foliage?.entry)
					}
				}
			}

			if (pairs.isEmpty()) {
				throw NoSuchElementException("Out of all possible combinations of tree stems and foliage, none match by shape (stems candidates: $validTreeStems; foliage candidates: $validFoliage)")
			}

			val treeVariants = biome.random.nextRange(variantsRange)

			if (treeVariants <= 0) {
				return BiomePlaceables.Tree(ImmutableList.of())
			}

			val trees = ArrayList<TreeVariant>()

			for (i in 0 until treeVariants) {
				val subTreeVariants = biome.random.nextRange(subVariantsRange)
				if (subTreeVariants <= 0) continue
				val (stem, foliage) = pairs.random(biome.random)

				val treeStemHueShift = treeStemHueShiftMax * biome.random.nextDouble(-1.0, 1.0)
				val treeFoliageHueShift = treeFoliageHueShiftMax * biome.random.nextDouble(-1.0, 1.0)

				if (foliage == null) {
					for (i2 in 0 until subTreeVariants) {
						trees.add(
							TreeVariant.create(
								stem,
								if (sameStemHueShift) treeStemHueShift else treeStemHueShiftMax * biome.random.nextDouble(
									-1.0,
									1.0
								)
							)
						)
					}
				} else {
					for (i2 in 0 until subTreeVariants) {
						trees.add(
							TreeVariant.create(
								stem,
								if (sameStemHueShift) treeStemHueShift else treeStemHueShiftMax * biome.random.nextDouble(
									-1.0,
									1.0
								),
								foliage,
								if (sameFoliageHueShift) treeFoliageHueShift else treeFoliageHueShiftMax * biome.random.nextDouble(
									-1.0,
									1.0
								)
							)
						)
					}
				}
			}

			return BiomePlaceables.Tree(ImmutableList.copyOf(trees))
		}
	}

	@JsonFactory
	data class BushData(
		val name: Registry.Ref<BushVariant.Data>,
		val baseHueShiftMax: Double = 0.0,
		val modHueShiftMax: Double = 0.0,
	)

	@JsonFactory
	data class Bush(val bushes: ImmutableSet<BushData> = ImmutableSet.of()) : DistributionItemData() {
		override val type: BiomePlacementItemType
			get() = BiomePlacementItemType.BUSH

		override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item {
			val valid = bushes.stream().filter { it.name.entry != null }.toList()

			if (valid.isEmpty()) {
				throw NoSuchElementException("None of bush variants are valid (candidates: $bushes)")
			}

			val (name, baseHueShiftMax, modHueShiftMax) = valid.random(biome.random)
			val data = name.entry!!.value
			val mod = if (data.mods.isNotEmpty()) data.mods.random(biome.random) else ""

			return BiomePlaceables.Bush(
				BushVariant.create(
					name.entry!!,
					biome.random.nextDouble(-1.0, 1.0) * baseHueShiftMax,
					mod,
					biome.random.nextDouble(-1.0, 1.0) * modHueShiftMax
				)
			)
		}
	}

	@JsonFactory
	data class ObjectPool(val pool: ImmutableList<Pair<Double, String>> = ImmutableList.of(), val parameters: JsonElement = JsonObject())

	@JsonFactory
	data class Object(val objectSets: ImmutableList<ObjectPool>) : DistributionItemData() {
		override val type: BiomePlacementItemType
			get() = BiomePlacementItemType.OBJECT

		override fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables.Item {
			if (objectSets.isEmpty()) {
				return BiomePlaceables.Object(WeightedList())
			}

			val rand = objectSets.random(biome.random)
			return BiomePlaceables.Object(
				WeightedList(
					rand.pool.stream().map { it.first to (it.second to rand.parameters) }
						.collect(ImmutableList.toImmutableList())
				)
			)
		}
	}

	// --------------- DISTRIBUTION
	abstract class DistributionData {
		abstract val type: BiomePlacementDistributionType
		abstract fun create(self: DistributionItem, biome: BiomeDefinition.CreationParams): BiomePlaceables.DistributionData
	}

	@JsonFactory
	data class RandomDistribution(val blockProbability: Double) : DistributionData() {
		override val type: BiomePlacementDistributionType
			get() = BiomePlacementDistributionType.RANDOM

		override fun create(
			self: DistributionItem,
			biome: BiomeDefinition.CreationParams
		): BiomePlaceables.DistributionData {
			return BiomePlaceables.DistributionData(
				distribution = BiomePlacementDistributionType.RANDOM,
				blockSeed = biome.random.nextLong(),
				blockProbability = blockProbability,
				randomItems = IntStream.range(0, self.variants)
					.mapToObj { self.data.create(biome) }
					.collect(ImmutableList.toImmutableList())
			)
		}
	}

	@JsonFactory
	data class PeriodicDistribution(
		val modulus: Int = 1,
		val octaves: Int = 1,
		val alpha: Double = 2.0,
		val beta: Double = 2.0,
		val modulusVariance: Double = 0.0,
		val densityPeriod: Double = 10.0,
		val densityOffset: Double = 2.0,
		val typePeriod: Double = 10.0,
		val noiseType: PerlinNoiseParameters.Type = PerlinNoiseParameters.Type.PERLIN,
		val noiseScale: Int = PerlinNoiseParameters.DEFAULT_SCALE,
	) : DistributionData() {
		override val type: BiomePlacementDistributionType
			get() = BiomePlacementDistributionType.PERIODIC

		override fun create(
			self: DistributionItem,
			biome: BiomeDefinition.CreationParams
		): BiomePlaceables.DistributionData {
			val modulusOffset = if (modulus == 0) 0 else biome.random.nextInt(-modulus, modulus)

			return BiomePlaceables.DistributionData(
				distribution = BiomePlacementDistributionType.PERIODIC,
				modulus = modulus,
				modulusOffset = modulusOffset,

				densityFunction = AbstractPerlinNoise.of(
					PerlinNoiseParameters(
						type = noiseType,
						scale = noiseScale,
						octaves = octaves,
						alpha = alpha,
						beta = beta,

						frequency = 1.0 / densityPeriod,
						amplitude = 1.0,
						bias = densityOffset,
						seed = biome.random.nextLong()
					)
				),

				modulusDistortion = AbstractPerlinNoise.of(
					PerlinNoiseParameters(
						type = noiseType,
						scale = noiseScale,
						octaves = octaves,
						alpha = alpha,
						beta = beta,

						frequency = 1.0 / modulus,
						amplitude = modulusVariance,
						bias = modulusVariance * 2.0,
						seed = biome.random.nextLong()
					)
				),

				weightedItems = IntStream.range(0, self.variants)
					.mapToObj { self.data.create(biome) }
					.map {
						it to AbstractPerlinNoise.of(
							PerlinNoiseParameters(
								type = noiseType,
								scale = noiseScale,
								octaves = octaves,
								alpha = alpha,
								beta = beta,

								frequency = 1.0 / typePeriod,
								amplitude = 1.0,
								bias = 0.0,
								seed = biome.random.nextLong(),
							)
						)
					}
					.collect(ImmutableList.toImmutableList())
			)
		}
	}

	fun create(biome: BiomeDefinition.CreationParams): BiomePlaceables {
		return BiomePlaceables(
			grassMod = NativeLegacy.TileMod(grassMod.random(biome.random) { null }?.entry),
			ceilingGrassMod = NativeLegacy.TileMod(ceilingGrassMod.random(biome.random) { null }?.entry),
			grassModDensity = grassModDensity,
			ceilingGrassModDensity = ceilingGrassModDensity,
			itemDistributions = items.stream()
				.map { it.create(biome) }
				.collect(ImmutableList.toImmutableList())
		)
	}
}