package ru.dbotthepony.mc.otm.android

import it.unimi.dsi.fastutil.io.FastByteArrayOutputStream
import net.minecraft.ChatFormatting
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.player.Player
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.common.util.INBTSerializable
import net.minecraftforge.eventbus.api.Cancelable
import net.minecraftforge.eventbus.api.Event
import net.minecraftforge.eventbus.api.Event.HasResult
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.capability.MatteryPlayerCapability
import ru.dbotthepony.mc.otm.capability.awareItemsStream
import ru.dbotthepony.mc.otm.core.TextComponent
import ru.dbotthepony.mc.otm.core.TranslatableComponent
import ru.dbotthepony.mc.otm.core.registryName
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.milliTime
import ru.dbotthepony.mc.otm.network.synchronizer.FieldSynchronizer
import ru.dbotthepony.mc.otm.triggers.AndroidResearchTrigger
import java.io.InputStream
import kotlin.math.absoluteValue

class AndroidResearch(val type: AndroidResearchType, val capability: MatteryPlayerCapability) : INBTSerializable<CompoundTag> {
	/**
	 * Fired on main event bus [MinecraftForge.EVENT_BUS]
	 */
	data class OnResearched(val research: AndroidResearch) : Event()

	/**
	 * Fired on main event bus [MinecraftForge.EVENT_BUS]
	 */
	data class OnUnResearched(val research: AndroidResearch) : Event()

	/**
	 * Fired on main event bus [MinecraftForge.EVENT_BUS]
	 */
	data class OnRefunded(val research: AndroidResearch) : Event()

	/**
	 * Fired on main event bus [MinecraftForge.EVENT_BUS]
	 */
	data class GatherTooltipsEvent(val research: AndroidResearch, val tooltips: MutableList<Component>) : Event()

	/**
	 * Fired on main event bus [MinecraftForge.EVENT_BUS]
	 */
	@HasResult
	data class ConsumeResearchCost(val research: AndroidResearch, val isSimulating: Boolean) : Event()

	val ply: Player get() = capability.ply

	val synchronizer = FieldSynchronizer()

	var isResearched by synchronizer.bool().property
		private set

	var tag = CompoundTag()
		private set

	/**
	 * Called when it is required to network everything again
	 */
	fun invalidateNetwork() {
		synchronizer.invalidate()
	}

	fun unResearch() {
		if (!isResearched) {
			return
		}

		onUnResearched()
		isResearched = false
	}

	fun onUnResearched() {
		for (result in type.results) {
			result.onUnResearched(this)
		}

		MinecraftForge.EVENT_BUS.post(OnUnResearched(this))
	}

	fun onResearched() {
		for (result in type.results) {
			result.onResearched(this)
		}

		MinecraftForge.EVENT_BUS.post(OnResearched(this))
	}

	/**
	 * Consumes required resources for researching this technology.
	 *
	 * Returns true if all required resources were consumed, false otherwise.
	 *
	 * This function MUST be atomic, hence it SHOULD NOT consume ANY resources if it returns false.
	 */
	fun consumeResearchCost(simulate: Boolean): Boolean {
		if (capability.ply.abilities.instabuild) {
			// creative
			return true
		}

		val event = ConsumeResearchCost(this, simulate)
		MinecraftForge.EVENT_BUS.post(event)

		if (event.result == Event.Result.ALLOW) {
			return true
		} else if (event.result == Event.Result.DENY) {
			return false
		}

		if (!simulate && !consumeResearchCost(true)) {
			return false
		}

		if (type.experienceLevels > 0) {
			if (capability.ply.experienceLevel < type.experienceLevels) {
				return false
			}

			if (!simulate) {
				capability.ply.giveExperienceLevels(-type.experienceLevels)
			}
		}

		for ((tag, count) in type.items) {
			var required = count

			for (invItem in ply.awareItemsStream(false)) {
				if (tag.test(invItem.itemStack)) {
					val toExtract = required.coerceAtMost(invItem.itemStack.count)
					required -= invItem.extract(toExtract, simulate).count

					if (required <= 0) {
						break
					}
				}
			}

			if (required > 0) {
				return false
			}
		}

		return true
	}

	/**
	 * Grants all (or some) resources back consumed by [consumeResearchCost].
	 *
	 * Returns true whenever player accepted all resources refunded, false otherwise.
	 */
	fun refund(): Boolean {
		if (type.experienceLevels > 0) {
			var experiencePoints = 0

			for (level in 0 until type.experienceLevels) {
				experiencePoints += when (level) {
					in 0 .. 14 -> 7 + level * 2
					in 15 .. 29 -> 37 + (level - 15) * 5
					else -> 112 + (level - 30) * 9
				}
			}

			capability.ply.giveExperiencePoints(experiencePoints)
		}

		for (item in type.items) {
			val stack = (item.first.items.firstOrNull { it.item.registryName?.namespace == OverdriveThatMatters.MOD_ID } ?: item.first.items.first()).copy()
			stack.count = item.second
			capability.ply.inventory.add(stack)

			if (!stack.isEmpty) {
				capability.ply.spawnAtLocation(stack)
			}
		}

		MinecraftForge.EVENT_BUS.post(OnRefunded(this))

		return true
	}

	fun collectNetworkPayload(): FastByteArrayOutputStream? {
		return synchronizer.collectNetworkPayload()
	}

	fun applyNetworkPayload(stream: InputStream) {
		synchronizer.read(stream)
	}

	val canResearch: Boolean get() {
		if (!consumeResearchCost(simulate = true)) {
			return false
		}

		if (!isPrerequisitesResearched || (!ply.isCreative && isAnyBlockerResearched)) {
			return false
		}

		return true
	}

	/**
	 * Determines if this research is already blocked directly by another research.
	 */
	val isAnyBlockerResearched: Boolean get() {
		for (research in type.flatBlockedBy) {
			if (capability.getResearch(research).isResearched) {
				return true
			}
		}

		return false
	}

	/**
	 * Determines if this research is already blocked by another research, directly or indirectly.
	 *
	 * This is slower than [isAnyBlockerResearched] and makes sense only to check this for visualization purposes,
	 * since indirect blocking research will just turn [isPrerequisitesResearched] false (under normal conditions).
	 */
	val isAnyBlockerResearchedIndirect: Boolean get() {
		for (research in type.allBlockedBy) {
			if (capability.getResearch(research).isResearched) {
				return true
			}
		}

		return false
	}

	/**
	 * Determines if this research' direct prerequisites are researched.
	 */
	val isPrerequisitesResearched: Boolean get() {
		for (research in type.flatPrerequisites) {
			if (!capability.getResearch(research).isResearched) {
				return false
			}
		}

		return true
	}

	/**
	 * Determines if this research' prerequisites are researched, direct or indirect.
	 *
	 * Doesn't make much sense to check for this, since all prerequisites *should* have their prerequisites
	 * researched and so on.
	 *
	 * Strictly speaking, if someone tamper with research tree and mark all direct prerequisites unlocked
	 * without unlocking their prerequisites, [isPrerequisitesResearched] will return true, but *this* will return *false*.
	 */
	val isPrerequisitesResearchedIndirect: Boolean get() {
		for (research in type.allPrerequisites) {
			if (!capability.getResearch(research).isResearched) {
				return false
			}
		}

		return true
	}

	fun research(force: Boolean = false): Boolean {
		if (isResearched) {
			return false
		}

		if (force || canResearch && consumeResearchCost(false)) {
			onResearched()
			isResearched = true

			if (ply is ServerPlayer) {
				AndroidResearchTrigger.trigger(ply as ServerPlayer, this.type)
			}

			return true
		}

		return false
	}

	/**
	 * List of all tooltip lines for this research
	 */
	val tooltipLines: List<Component> get() {
		val lines = ArrayList<Component>()
		lines.add(type.displayName)

		for (line in type.description) {
			line.addLines(this, lines)
		}

		MinecraftForge.EVENT_BUS.post(GatherTooltipsEvent(this, lines))

		return lines
	}

	/**
	 * List of all tooltip lines for this research, for local player at research screen (clientside only)
	 */
	val screenTooltipLines: List<Component> get() {
		val builder = tooltipLines as ArrayList<Component>

		if (type.experienceLevels != 0) {
			builder.add(
				TranslatableComponent("otm.android_station.research.xp_cost", TextComponent(type.experienceLevels.toString()).withStyle(
					if (capability.ply.experienceLevel >= type.experienceLevels)
						ChatFormatting.DARK_GREEN
					else
						ChatFormatting.DARK_RED
				)).withStyle(ChatFormatting.GRAY))
		}

		for (value in this.type.flatPrerequisites) {
			val instance = capability.getResearch(value)

			builder.add(
				TranslatableComponent("android_research.status.requires", instance.screenTooltipHeader.copy().withStyle(
					if (instance.isResearched)
						ChatFormatting.DARK_GREEN
					else
						ChatFormatting.DARK_RED
				)).withStyle(ChatFormatting.GRAY))
		}

		for (value in this.type.flatBlockedBy) {
			builder.add(TranslatableComponent("android_research.status.blocked_by", capability.getResearch(value).screenTooltipHeader.copy().withStyle(ChatFormatting.DARK_RED)).withStyle(ChatFormatting.GRAY))
		}

		for (value in this.type.flatBlocking) {
			builder.add(TranslatableComponent("android_research.status.blocks", capability.getResearch(value).screenTooltipHeader.copy().withStyle(ChatFormatting.YELLOW)).withStyle(ChatFormatting.GRAY))
		}

		for ((tag, count) in type.items) {
			var required = count

			for (invItem in ply.awareItemsStream(false)) {
				if (tag.test(invItem.itemStack)) {
					val toExtract = required.coerceAtMost(invItem.itemStack.count)
					required -= invItem.extract(toExtract, true).count

					if (required <= 0) {
						break
					}
				}
			}

			val chooseItem = tag.items[((milliTime / 800L).absoluteValue % tag.items.size).toInt()]
			val suffix = if (tag.items.size > 1) "_any" else ""

			if (required > 0) {
				if (count == 1) {
					builder.add(TranslatableComponent("android_research.status.requires_item$suffix", chooseItem.hoverName.copy().withStyle(ChatFormatting.DARK_RED)).withStyle(ChatFormatting.GRAY))
				} else {
					builder.add(TranslatableComponent("android_research.status.requires_item_multiple${suffix}0", chooseItem.hoverName.copy().withStyle(ChatFormatting.DARK_RED), TextComponent(count.toString()).withStyle(ChatFormatting.YELLOW), TextComponent(required.toString()).withStyle(ChatFormatting.YELLOW)).withStyle(ChatFormatting.GRAY))
				}
			} else {
				if (count == 1) {
					builder.add(TranslatableComponent("android_research.status.requires_item$suffix", chooseItem.hoverName.copy().withStyle(ChatFormatting.DARK_GREEN)).withStyle(ChatFormatting.GRAY))
				} else {
					builder.add(TranslatableComponent("android_research.status.requires_item_multiple${suffix}1", chooseItem.hoverName.copy().withStyle(ChatFormatting.DARK_GREEN), TextComponent(count.toString()).withStyle(ChatFormatting.YELLOW)).withStyle(ChatFormatting.GRAY))
				}
			}
		}

		return builder
	}

	/**
	 * This should return whatever can be used by other code to display this research's name,
	 * for local player at research screen (clientside only)
	 */
	val screenTooltipHeader: Component get() = type.displayName

	override fun serializeNBT(): CompoundTag {
		return CompoundTag().also {
			it["researched"] = isResearched
			it["tag"] = tag
		}
	}

	override fun deserializeNBT(nbt: CompoundTag) {
		isResearched = nbt.getBoolean("researched")
		tag = nbt.getCompound("tag")
	}
}
