package ru.dbotthepony.kstarbound

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.kommons.util.KOptional
import ru.dbotthepony.kstarbound.defs.AssetReference
import ru.dbotthepony.kstarbound.defs.Json2Function
import ru.dbotthepony.kstarbound.defs.JsonConfigFunction
import ru.dbotthepony.kstarbound.defs.JsonFunction
import ru.dbotthepony.kstarbound.defs.MarkovTextGenerator
import ru.dbotthepony.kstarbound.defs.Species
import ru.dbotthepony.kstarbound.defs.StatusEffectDefinition
import ru.dbotthepony.kstarbound.defs.ThingDescription
import ru.dbotthepony.kstarbound.defs.item.TreasurePoolDefinition
import ru.dbotthepony.kstarbound.defs.monster.MonsterSkillDefinition
import ru.dbotthepony.kstarbound.defs.monster.MonsterTypeDefinition
import ru.dbotthepony.kstarbound.defs.npc.NpcTypeDefinition
import ru.dbotthepony.kstarbound.defs.npc.TenantDefinition
import ru.dbotthepony.kstarbound.defs.`object`.ObjectDefinition
import ru.dbotthepony.kstarbound.defs.actor.player.TechDefinition
import ru.dbotthepony.kstarbound.defs.animation.ParticleConfig
import ru.dbotthepony.kstarbound.defs.dungeon.DungeonDefinition
import ru.dbotthepony.kstarbound.defs.projectile.ProjectileDefinition
import ru.dbotthepony.kstarbound.defs.quest.QuestTemplate
import ru.dbotthepony.kstarbound.defs.tile.LiquidDefinition
import ru.dbotthepony.kstarbound.defs.tile.RenderParameters
import ru.dbotthepony.kstarbound.defs.tile.TileDamageConfig
import ru.dbotthepony.kstarbound.defs.tile.TileModifierDefinition
import ru.dbotthepony.kstarbound.defs.tile.TileDefinition
import ru.dbotthepony.kstarbound.defs.world.BushVariant
import ru.dbotthepony.kstarbound.defs.world.GrassVariant
import ru.dbotthepony.kstarbound.defs.world.TreeVariant
import ru.dbotthepony.kstarbound.defs.world.BiomeDefinition
import ru.dbotthepony.kstarbound.item.ItemRegistry
import ru.dbotthepony.kstarbound.json.JsonPatch
import ru.dbotthepony.kstarbound.json.builder.JsonFactory
import ru.dbotthepony.kstarbound.world.terrain.TerrainSelectorType
import ru.dbotthepony.kstarbound.util.AssetPathStack
import ru.dbotthepony.kstarbound.world.physics.CollisionType
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import kotlin.collections.ArrayList

object Registries {
	private val LOGGER = LogManager.getLogger()

	private val registriesInternal = ArrayList<Registry<*>>()
	val registries: List<Registry<*>> = Collections.unmodifiableList(registriesInternal)
	private val adapters = ArrayList<TypeAdapterFactory>()

	fun registerAdapters(gsonBuilder: GsonBuilder) {
		adapters.forEach { gsonBuilder.registerTypeAdapterFactory(it) }
	}

	val tiles = Registry<TileDefinition>("tiles").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val tileModifiers = Registry<TileModifierDefinition>("tile modifiers").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val liquid = Registry<LiquidDefinition>("liquid").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val species = Registry<Species>("species").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val statusEffects = Registry<StatusEffectDefinition>("status effect").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val particles = Registry<ParticleConfig>("particle").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val questTemplates = Registry<QuestTemplate>("quest template").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val techs = Registry<TechDefinition>("tech").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val jsonFunctions = Registry<JsonFunction>("json function").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val json2Functions = Registry<Json2Function>("json 2function").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val jsonConfigFunctions = Registry<JsonConfigFunction>("json config function").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val npcTypes = Registry<NpcTypeDefinition>("npc type").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val projectiles = Registry<ProjectileDefinition>("projectile").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val tenants = Registry<TenantDefinition>("tenant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val treasurePools = Registry<TreasurePoolDefinition>("treasure pool").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val monsterSkills = Registry<MonsterSkillDefinition>("monster skill").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val monsterTypes = Registry<MonsterTypeDefinition>("monster type").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val worldObjects = Registry<ObjectDefinition>("world object").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val biomes = Registry<BiomeDefinition>("biome").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val terrainSelectors = Registry<TerrainSelectorType.Factory<*, *>>("terrain selector").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val grassVariants = Registry<GrassVariant.Data>("grass variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val treeStemVariants = Registry<TreeVariant.StemData>("tree stem variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val treeFoliageVariants = Registry<TreeVariant.FoliageData>("tree foliage variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val bushVariants = Registry<BushVariant.Data>("bush variant").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val dungeons = Registry<DungeonDefinition>("dungeon").also(registriesInternal::add).also { adapters.add(it.adapter()) }
	val markovGenerators = Registry<MarkovTextGenerator>("markov text generator").also(registriesInternal::add).also { adapters.add(it.adapter()) }

	private fun <T> key(mapper: (T) -> String): (T) -> Pair<String, KOptional<Int?>> {
		return { mapper.invoke(it) to KOptional() }
	}

	private fun <T> key(mapper: (T) -> String, mapperInt: (T) -> Int?): (T) -> Pair<String, KOptional<Int?>> {
		return { mapper.invoke(it) to KOptional(mapperInt.invoke(it)) }
	}

	fun validate(): CompletableFuture<Boolean> {
		val futures = ArrayList<CompletableFuture<Boolean>>()

		for (registry in registriesInternal)
			futures.add(CompletableFuture.supplyAsync { registry.validate() })

		return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { futures.all { it.get() } }
	}

	private inline fun <reified T : Any> loadRegistry(
		registry: Registry<T>,
		patches: Map<String, Collection<IStarboundFile>>,
		files: Collection<IStarboundFile>,
		noinline keyProvider: (T) -> Pair<String, KOptional<Int?>>,
		noinline after: (T, IStarboundFile) -> Unit = { _, _ -> }
	): List<Future<*>> {
		val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }

		return files.map { listedFile ->
			Starbound.EXECUTOR.submit {
				try {
					AssetPathStack(listedFile.computeDirectory()) {
						val elem = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()])
						val read = adapter.fromJsonTree(elem)
						val keys = keyProvider(read)

						after(read, listedFile)

						registry.add {
							registry.add(
								key = keys.first,
								value = read,
								id = keys.second,
								json = elem,
								file = listedFile
							)
						}
					}
				} catch (err: Throwable) {
					LOGGER.error("Loading ${registry.name} definition file $listedFile", err)
				}
			}
		}
	}

	fun finishLoad() {
		registriesInternal.forEach { it.finishLoad() }
	}

	fun load(fileTree: Map<String, Collection<IStarboundFile>>, patchTree: Map<String, List<IStarboundFile>>): List<Future<*>> {
		val tasks = ArrayList<Future<*>>()

		tasks.addAll(ItemRegistry.load(fileTree, patchTree))

		tasks.addAll(loadTerrainSelectors(fileTree, patchTree))

		tasks.addAll(loadRegistry(tiles, patchTree, fileTree["material"] ?: listOf(), key(TileDefinition::materialName, TileDefinition::materialId)))
		tasks.addAll(loadRegistry(tileModifiers, patchTree, fileTree["matmod"] ?: listOf(), key(TileModifierDefinition::modName, TileModifierDefinition::modId)))
		tasks.addAll(loadRegistry(liquid, patchTree, fileTree["liquid"] ?: listOf(), key(LiquidDefinition::name, LiquidDefinition::liquidId)))

		tasks.add(loadMetaMaterials())
		tasks.addAll(loadRegistry(dungeons, patchTree, fileTree["dungeon"] ?: listOf(), key(DungeonDefinition::name)))

		tasks.addAll(loadRegistry(worldObjects, patchTree, fileTree["object"] ?: listOf(), key(ObjectDefinition::objectName)))
		tasks.addAll(loadRegistry(statusEffects, patchTree, fileTree["statuseffect"] ?: listOf(), key(StatusEffectDefinition::name)))
		tasks.addAll(loadRegistry(species, patchTree, fileTree["species"] ?: listOf(), key(Species::kind)))
		tasks.addAll(loadRegistry(particles, patchTree, fileTree["particle"] ?: listOf(), { (it.kind ?: throw NullPointerException("Missing 'kind' value")) to KOptional() }))
		tasks.addAll(loadRegistry(questTemplates, patchTree, fileTree["questtemplate"] ?: listOf(), key(QuestTemplate::id)))
		tasks.addAll(loadRegistry(techs, patchTree, fileTree["tech"] ?: listOf(), key(TechDefinition::name)))
		tasks.addAll(loadRegistry(npcTypes, patchTree, fileTree["npctype"] ?: listOf(), key(NpcTypeDefinition::type)))
		tasks.addAll(loadRegistry(monsterSkills, patchTree, fileTree["monsterskill"] ?: listOf(), key(MonsterSkillDefinition::name)))
		tasks.addAll(loadRegistry(biomes, patchTree, fileTree["biome"] ?: listOf(), key(BiomeDefinition::name)))
		tasks.addAll(loadRegistry(grassVariants, patchTree, fileTree["grass"] ?: listOf(), key(GrassVariant.Data::name)))
		tasks.addAll(loadRegistry(treeStemVariants, patchTree, fileTree["modularstem"] ?: listOf(), key(TreeVariant.StemData::name)))
		tasks.addAll(loadRegistry(treeFoliageVariants, patchTree, fileTree["modularfoliage"] ?: listOf(), key(TreeVariant.FoliageData::name)))
		tasks.addAll(loadRegistry(bushVariants, patchTree, fileTree["bush"] ?: listOf(), key(BushVariant.Data::name)))
		tasks.addAll(loadRegistry(markovGenerators, patchTree, fileTree["namesource"] ?: listOf(), key(MarkovTextGenerator::name)))

		tasks.addAll(loadCombined(jsonFunctions, fileTree["functions"] ?: listOf(), patchTree))
		tasks.addAll(loadCombined(json2Functions, fileTree["2functions"] ?: listOf(), patchTree))
		tasks.addAll(loadCombined(jsonConfigFunctions, fileTree["configfunctions"] ?: listOf(), patchTree))
		tasks.addAll(loadCombined(treasurePools, fileTree["treasurepools"] ?: listOf(), patchTree) { name = it })

		return tasks
	}

	private inline fun <reified T : Any> loadCombined(registry: Registry<T>, files: Collection<IStarboundFile>, patches: Map<String, Collection<IStarboundFile>>, noinline transform: T.(String) -> Unit = {}): List<Future<*>> {
		val adapter by lazy { Starbound.gson.getAdapter(T::class.java) }

		return files.map { listedFile ->
			Starbound.EXECUTOR.submit {
				try {
					val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()]) as JsonObject

					for ((k, v) in json.entrySet()) {
						try {
							val value = adapter.fromJsonTree(v)
							transform(value, k)

							registry.add {
								registry.add(k, value, v, listedFile)
							}
						} catch (err: Exception) {
							LOGGER.error("Loading ${registry.name} definition $k from file $listedFile", err)
						}
					}
				} catch (err: Exception) {
					LOGGER.error("Loading ${registry.name} definition $listedFile", err)
				}
			}
		}
	}

	private fun loadTerrainSelector(listedFile: IStarboundFile, type: TerrainSelectorType?, patches: Map<String, Collection<IStarboundFile>>) {
		try {
			val json = JsonPatch.apply(Starbound.ELEMENTS_ADAPTER.read(listedFile.jsonReader()), patches[listedFile.computeFullPath()]) as JsonObject
			val name = json["name"]?.asString ?: throw JsonSyntaxException("Missing 'name' field")
			val factory = TerrainSelectorType.factory(json, false, type)

			terrainSelectors.add {
				terrainSelectors.add(name, factory)
			}
		} catch (err: Exception) {
			LOGGER.error("Loading terrain selector $listedFile", err)
		}
	}

	private fun loadTerrainSelectors(files: Map<String, Collection<IStarboundFile>>, patches: Map<String, Collection<IStarboundFile>>): List<Future<*>> {
		val tasks = ArrayList<Future<*>>()

		tasks.addAll((files["terrain"] ?: listOf()).map { listedFile ->
			Starbound.EXECUTOR.submit {
				loadTerrainSelector(listedFile, null, patches)
			}
		})

		// legacy files
		for (type in TerrainSelectorType.entries) {
			tasks.addAll((files[type.jsonName.lowercase()] ?: listOf()).map { listedFile ->
				Starbound.EXECUTOR.submit {
					loadTerrainSelector(listedFile, type, patches)
				}
			})
		}

		return tasks
	}

	@JsonFactory
	data class MetaMaterialDef(
		val materialId: Int,
		val name: String,
		val collisionKind: CollisionType,
		val blocksLiquidFlow: Boolean = collisionKind.isSolidCollision,
		val isConnectable: Boolean = true,
		val supportsMods: Boolean = false,
	)

	private fun loadMetaMaterials(): Future<*> {
		return Starbound.EXECUTOR.submit {
			val read = Starbound.loadJsonAsset("/metamaterials.config") ?: return@submit
			val read2 = Starbound.gson.getAdapter(object : TypeToken<ImmutableList<MetaMaterialDef>>() {}).fromJsonTree(read)

			for (def in read2) {
				tiles.add {
					tiles.add(key = "metamaterial:${def.name}", id = KOptional(def.materialId), value = TileDefinition(
						isMeta = true,
						materialId = def.materialId,
						materialName = "metamaterial:${def.name}",
						descriptionData = ThingDescription.EMPTY,
						category = "meta",
						renderTemplate = AssetReference.empty(),
						renderParameters = RenderParameters.META,
						isConnectable = def.isConnectable,
						supportsMods = def.supportsMods,
						damageTable = AssetReference(TileDamageConfig(
							damageFactors = ImmutableMap.of(),
							damageRecovery = Double.MAX_VALUE,
							maximumEffectTime = 0.0,
							totalHealth = Double.MAX_VALUE,
							harvestLevel = Int.MAX_VALUE,
						))
					))
				}
			}
		}
	}
}
