package ru.dbotthepony.mc.otm.android

import com.google.common.collect.ImmutableList
import com.google.gson.JsonObject
import com.google.gson.JsonSyntaxException
import com.mojang.datafixers.util.Either
import com.mojang.serialization.Codec
import com.mojang.serialization.codecs.ListCodec
import com.mojang.serialization.codecs.RecordCodecBuilder
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import net.minecraft.network.chat.Component
import net.minecraft.network.chat.ComponentContents
import net.minecraft.network.chat.MutableComponent
import net.minecraft.network.chat.contents.TranslatableContents
import net.minecraft.resources.ResourceLocation
import net.minecraft.tags.TagKey
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.crafting.Ingredient
import net.minecraft.world.level.ItemLike
import net.minecraftforge.registries.ForgeRegistries
import ru.dbotthepony.mc.otm.client.render.AbstractMatterySprite
import ru.dbotthepony.mc.otm.client.render.SpriteType
import ru.dbotthepony.mc.otm.core.collect.ListSet
import ru.dbotthepony.mc.otm.core.TranslatableComponent
import ru.dbotthepony.mc.otm.core.isActuallyEmpty
import ru.dbotthepony.mc.otm.data.ComponentCodec
import ru.dbotthepony.mc.otm.data.IngredientCodec
import ru.dbotthepony.mc.otm.data.JsonElementCodec
import ru.dbotthepony.mc.otm.isClient
import java.util.Optional
import java.util.function.Function
import java.util.stream.Stream
import kotlin.collections.ArrayList
import kotlin.collections.HashSet
import kotlin.jvm.optionals.getOrNull

private fun findPrerequisites(
	initial: Collection<AndroidResearchType>,
	add: MutableSet<AndroidResearchType> = ObjectOpenHashSet(),
	top: Boolean = true
): Set<AndroidResearchType> {
	for (value in initial) {
		if (!top) {
			add.add(value)
		}

		findPrerequisites(value.resolvedPrerequisites, add, false)
	}

	return add
}

private fun findAllPrerequisites(
	initial: Collection<AndroidResearchType>,
	add: MutableSet<AndroidResearchType> = ObjectOpenHashSet(),
): Set<AndroidResearchType> {
	for (value in initial) {
		add.add(value)
		findAllPrerequisites(value.flatPrerequisites, add)
	}

	return add
}

private fun findAllChildren(
	initial: Collection<AndroidResearchType>,
	add: MutableSet<AndroidResearchType> = ObjectOpenHashSet(),
): Set<AndroidResearchType> {
	for (value in initial) {
		add.add(value)
		findAllChildren(value.flatUnlocks, add)
	}

	return add
}

class AndroidResearchType(
	val id: ResourceLocation,
	prerequisites: Collection<Reference>,
	blockedBy: Collection<Reference>,

	items: Collection<Pair<Ingredient, Int>>,
	results: Collection<AndroidResearchResult>,

	description: Collection<AndroidResearchDescription>,

	val experienceLevels: Int = 0,
	private val customName: Component? = null,

	// ok
	val skinIcon: JsonObject? = null,
	val itemIcon: Item? = null,
	iconText: Component? = null,
) {
	private constructor(
		id: ResourceLocation,
		prerequisites: Collection<Reference>,
		blockedBy: Collection<Reference>,

		items: Collection<Pair<Ingredient, Int>>,
		results: Collection<AndroidResearchResult>,

		description: Collection<AndroidResearchDescription>,

		experienceLevels: Int,
		customName: Optional<Component>,

		// ok
		skinIcon: Optional<JsonObject>,
		itemIcon: Optional<Item>,
		iconText: Optional<Component>,
	) : this(id, prerequisites, blockedBy, items, results, description, experienceLevels, customName.getOrNull(), skinIcon.getOrNull(), itemIcon.getOrNull(), iconText.getOrNull())

	private val iconTextValue = iconText?.copy()
	val iconText get() = iconTextValue?.copy()

	val resolvedSkinIcon by lazy {
		check(isClient) { "Invalid realm" }
		if (skinIcon != null)
			SpriteType.fromJson(skinIcon)
		else
			null
	}

	data class Reference(
		val id: ResourceLocation,
		val optional: Boolean = false
	) {
		companion object {
			val CODEC: Codec<Reference> by lazy {
				Codec
					.either(ResourceLocation.CODEC, RecordCodecBuilder.create<Reference> {
						it.group(
							ResourceLocation.CODEC.fieldOf("id").forGetter(Reference::id),
							Codec.BOOL.optionalFieldOf("optional", false).forGetter(Reference::optional)
						).apply(it, ::Reference)
					})
					.xmap(
						{ c -> c.map(::Reference, Function.identity()) },
						{ c -> if (c.optional) Either.right(c) else Either.left(c.id) }
					)
			}
		}
	}

	val researchTreeDepth: Int by lazy {
		if (flatPrerequisites.isEmpty()) {
			return@lazy 0
		}

		var depth = 1

		for (value in flatPrerequisites) {
			depth = depth.coerceAtLeast(value.researchTreeDepth + 1)
		}

		return@lazy depth
	}

	/**
	 * Prerequisites as-is.
	 *
	 * Please avoid having more than one prerequisite, as this case doesn't have proper research tree
	 * rendering code (yet).
	 */
	val prerequisites: List<Reference> = ImmutableList.copyOf(prerequisites)

	/**
	 * Blocked by list as-is
	 */
	val blockedBy: List<Reference> = ImmutableList.copyOf(blockedBy)

	val resolvedPrerequisites: List<AndroidResearchType> by lazy {
		ImmutableList.copyOf(this.prerequisites.mapNotNull { AndroidResearchManager[it.id].also { e -> if (e == null && it.optional) throw NoSuchElementException("Unable to find research ${it.id}") } })
	}

	val resolvedBlockedBy: List<AndroidResearchType> by lazy {
		ImmutableList.copyOf(this.blockedBy.mapNotNull { AndroidResearchManager[it.id].also { e -> if (e == null && it.optional) throw NoSuchElementException("Unable to find research ${it.id}") } })
	}

	private val definedItems: List<Pair<Ingredient, Int>> = ImmutableList.copyOf(items)

	val results: ImmutableList<AndroidResearchResult> = ImmutableList.copyOf(results)
	val items: Stream<out Pair<Ingredient, Int>> get() = definedItems.stream().filter { !it.first.isActuallyEmpty }
	val description: ImmutableList<AndroidResearchDescription> = ImmutableList.copyOf(description)

	/**
	 * Flat list of research preceding this research.
	 *
	 * This list won't contain research following the same research path twice (guaranteed to be flat).
	 *
	 * E.g.
	 *
	 * * C depends on B
	 * * B depends on A
	 *
	 * C specify both B and A as it's prerequisites, [flatPrerequisites] will contain only B, when [prerequisites] will contain
	 * both B and A
	 *
	 * Returns list which also doubles as set (for contains method).
	 */
	val flatPrerequisites: List<AndroidResearchType> by lazy {
		val parentPrerequisites = findPrerequisites(resolvedPrerequisites)
		val builder = ImmutableList.builder<AndroidResearchType>()

		for (value in resolvedPrerequisites) {
			if (value !in parentPrerequisites) {
				builder.add(value)
			}
		}

		ListSet(builder.build())
	}

	/**
	 * All prerequisite research, including indirect ones
	 *
	 * Returns list which also doubles as set (for contains method).
	 */
	val allPrerequisites: List<AndroidResearchType> by lazy {
		ListSet(findAllPrerequisites(flatPrerequisites))
	}

	/**
	 * Flat list of research blocked by this research.
	 *
	 * This list won't contain children of any blocked research (guaranteed to be flat).
	 *
	 * E.g.
	 *
	 * * C depends on B
	 * * B is blocked by A
	 * * C is blocked by A
	 *
	 * Both C and B specify A as it's blockers, [flatBlocking] will contain only B, because C is blocked by A through B.
	 *
	 * Returns list which also doubles as set (for contains method).
	 */
	val flatBlocking: List<AndroidResearchType> by lazy {
		val list = ImmutableList.builder<AndroidResearchType>()

		for (research in AndroidResearchManager.researchMap.values) {
			if (this in research.resolvedBlockedBy) {
				var hit = false

				for (parent in research.allPrerequisites) {
					if (this in parent.resolvedBlockedBy) {
						hit = true
						break
					}
				}

				if (!hit) {
					list.add(research)
				}
			}
		}

		ListSet(list.build())
	}

	/**
	 * Flat list of research blocking this research.
	 *
	 * This list won't contain any research blocked by this' research parents.
	 *
	 * E.g.
	 *
	 * * C depends on B
	 * * B is blocked by A
	 * * C is blocked by A
	 *
	 * Both B and C are blocked by A, but C requires B, so C's [flatBlockedBy] will not contain A, but B's will.
	 *
	 * Returns list which also doubles as set (for contains method).
	 */
	val flatBlockedBy: ListSet<AndroidResearchType> by lazy {
		val list = ImmutableList.builder<AndroidResearchType>()

		for (blocker in resolvedBlockedBy) {
			var hit = false

			for (research in allPrerequisites) {
				if (blocker in research.flatBlockedBy) {
					hit = true
					break
				}
			}

			if (!hit) {
				list.add(blocker)
			}
		}

		ListSet(list.build())
	}

	/**
	 * All research blocking this research, even indirect ones (e.g. blocked by through one of parent research).
	 *
	 * Returns list which also doubles as set (for contains method).
	 */
	val allBlockedBy: ListSet<AndroidResearchType> by lazy {
		val list = HashSet<AndroidResearchType>()

		list.addAll(flatBlockedBy)

		for (parent in allPrerequisites) {
			list.addAll(parent.flatBlockedBy)
		}

		ListSet(list)
	}

	/**
	 * All research directly unlocked by this research.
	 *
	 * Returns list which also doubles as set (for contains method).
	 */
	val flatUnlocks: List<AndroidResearchType> by lazy {
		val list = ImmutableList.builder<AndroidResearchType>()

		for (research in AndroidResearchManager.researchMap.values) {
			if (this in research.flatPrerequisites) {
				list.add(research)
			}
		}

		ListSet(list.build())
	}

	/**
	 * All research unlocked by this research, including indirect ones
	 *
	 * Returns list which also doubles as set (for contains method).
	 */
	val allUnlocks: List<AndroidResearchType> by lazy {
		ListSet(findAllChildren(flatUnlocks))
	}

	/**
	 * All research blocked by this research, including indirect ones.
	 *
	 * Returns list which also doubles as set (for contains method)
	 */
	val allBlocking: List<AndroidResearchType> by lazy {
		val set = HashSet<AndroidResearchType>()

		for (research in flatBlocking) {
			set.add(research)
			set.addAll(research.allUnlocks)
		}

		for (research in allPrerequisites) {
			for (research2 in research.flatBlocking) {
				set.add(research2)
				set.addAll(research2.allUnlocks)
			}
		}

		ListSet(set)
	}

	val displayId by lazy {
		return@lazy "android_research.${id.namespace}.${id.path}".intern()
	}

	val descriptionId by lazy {
		return@lazy "$displayId.description".intern()
	}

	val displayContents: ComponentContents by lazy {
		TranslatableContents(displayId, null, arrayOf())
	}

	val displayName: Component get() {
		return customName?.copy() ?: MutableComponent.create(displayContents)
	}

	fun validate() {
		resolvedBlockedBy
		resolvedPrerequisites
	}

	companion object {
		val CODEC: Codec<AndroidResearchType> by lazy {
			RecordCodecBuilder.create {
				it.group(
					ResourceLocation.CODEC.fieldOf("id").forGetter(AndroidResearchType::id),
					ListCodec(Reference.CODEC).fieldOf("prerequisites").forGetter(AndroidResearchType::prerequisites),
					ListCodec(Reference.CODEC).fieldOf("blockedBy").forGetter(AndroidResearchType::blockedBy),
					ListCodec(
						RecordCodecBuilder.create<Pair<Ingredient, Int>> {
							it.group(
								IngredientCodec.fieldOf("item").forGetter { it.first },
								Codec.intRange(1, Int.MAX_VALUE).optionalFieldOf("count", 1).forGetter { it.second }
							).apply(it, ::Pair)
						}
					).fieldOf("items").forGetter { it.definedItems },
					ListCodec(AndroidResearchResult.CODEC).fieldOf("results").forGetter(AndroidResearchType::results),
					ListCodec(AndroidResearchDescription.CODEC).fieldOf("description").forGetter(AndroidResearchType::description),
					Codec.intRange(0, Int.MAX_VALUE).fieldOf("experienceLevels").forGetter(AndroidResearchType::experienceLevels),
					ComponentCodec.optionalFieldOf("customName").forGetter { Optional.ofNullable(it.customName) },
					JsonElementCodec.xmap({ it as? JsonObject ?: throw JsonSyntaxException("Not a json object: $it") }, { it }).optionalFieldOf("skinIcon").forGetter { Optional.ofNullable(it.skinIcon) },
					ForgeRegistries.ITEMS.codec.optionalFieldOf("itemIcon").forGetter { Optional.ofNullable(it.itemIcon) },
					ComponentCodec.optionalFieldOf("iconTextValue").forGetter { Optional.ofNullable(it.iconTextValue) },
				).apply(it, ::AndroidResearchType)
			}
		}
	}

	@Suppress("unused")
	class Builder(
		val id: ResourceLocation,
		var experience: Int = 0,
		var customName: Component? = null,
		description: MutableList<AndroidResearchDescription>? = null,
		var itemIcon: Item? = null,
		var skinIcon: AbstractMatterySprite? = null,
		var iconText: Component? = null,
	) {
		val description = ArrayList<AndroidResearchDescription>(description ?: listOf())
		val results = ArrayList<AndroidResearchResult>()

		private val items = ArrayList<Pair<Ingredient, Int>>()
		private val prerequisites = ArrayList<Reference>()
		private val blockers = ArrayList<Reference>()

		fun withIconText(icon: Component? = null): Builder {
			this.iconText = icon
			return this
		}

		fun withIcon(icon: AbstractMatterySprite? = null): Builder {
			this.skinIcon = icon
			this.itemIcon = null
			return this
		}

		fun withIcon(icon: Item? = null): Builder {
			this.itemIcon = icon
			this.skinIcon = null
			return this
		}

		fun withIcon(): Builder {
			this.itemIcon = null
			this.skinIcon = null
			return this
		}

		fun withName(customName: Component): Builder {
			this.customName = customName
			return this
		}

		fun clearDescription(): Builder {
			this.description.clear()
			return this
		}

		fun withDescription(): Builder {
			return withDescription(TranslatableComponent("android_research.${id.namespace}.${id.path}.description"))
		}

		fun withDescription(range: IntRange): Builder {
			for (i in range) {
				withDescription(TranslatableComponent("android_research.${id.namespace}.${id.path}.description$i"))
			}

			return this
		}

		fun withExperience(experience: Int): Builder {
			this.experience = experience
			return this
		}

		fun withDescription(vararg description: Component): Builder {
			for (component in description)
				withDescription(PlainAndroidResearchDescription.Instance(component))

			return this
		}

		fun withDescription(description: Collection<Component>): Builder {
			for (component in description)
				withDescription(PlainAndroidResearchDescription.Instance(component))

			return this
		}

		fun withDescription(vararg description: AndroidResearchDescription): Builder {
			this.description.addAll(description)
			return this
		}

		/**
		 * Please avoid having multiple prerequisites as case with more than 1 prerequisite does not have proper
		 * research tree render logic (yet).
		 */
		@JvmOverloads
		fun addPrerequisite(id: ResourceLocation, optional: Boolean = true): Builder {
			prerequisites.add(Reference(id, optional))
			return this
		}

		@JvmOverloads
		fun addBlocker(id: ResourceLocation, optional: Boolean = true): Builder {
			blockers.add(Reference(id, optional))
			return this
		}

		/**
		 * Please avoid having multiple prerequisites as case with more than 1 prerequisite does not have proper
		 * research tree render logic (yet).
		 */
		fun addPrerequisite(type: AndroidResearchType, optional: Boolean = false) = addPrerequisite(type.id, optional)
		fun addBlocker(type: AndroidResearchType, optional: Boolean = false) = addBlocker(type.id, optional)

		fun addResult(result: AndroidResearchResult): Builder {
			results.add(result)
			return this
		}

		fun addFeatureResult(id: ResourceLocation, optional: Boolean = false): Builder {
			return addResult(AndroidResearchResult.Feature(id, optional))
		}

		fun addFeatureResult(feature: AndroidFeatureType<*>, optional: Boolean = false): Builder {
			return addFeatureResult(feature.registryName!!, optional)
		}

		fun addFeatureLevel(id: ResourceLocation, optional: Boolean = false, levels: Int = 1): Builder {
			return addResult(AndroidResearchResult.FeatureLevel(id, optional, levels))
		}

		fun addFeatureLevel(feature: AndroidFeatureType<*>, optional: Boolean = false, levels: Int = 1): Builder {
			return addFeatureLevel(feature.registryName!!, optional, levels)
		}

		fun addItem(cost: ItemStack): Builder {
			require(!cost.isEmpty) { "Provided stack is empty" }
			items.add(Ingredient.of(cost) to cost.count)
			return this
		}

		fun addItem(cost: ItemLike, count: Int = 1): Builder {
			items.add(Ingredient.of(cost) to count)
			return this
		}

		fun addItem(cost: Ingredient, count: Int = 1): Builder {
			require(count >= 1) { "Invalid count $count" }
			items.add(cost to count)
			return this
		}

		fun addItem(cost: TagKey<Item>, count: Int = 1): Builder {
			require(count >= 1) { "Invalid count $count" }
			items.add(Ingredient.of(cost) to count)
			return this
		}

		fun build(validate: Boolean = false): AndroidResearchType {
			return AndroidResearchType(
				id = id,
				prerequisites = prerequisites,
				blockedBy = blockers,
				items = items,
				results = results,
				description = description,
				experienceLevels = experience,
				customName = customName,
				skinIcon = skinIcon?.toJson(),
				itemIcon = itemIcon,
				iconText = iconText
			).also { if (validate) it.validate() }
		}
	}
}
