package ru.dbotthepony.mc.otm.item

import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet
import net.minecraft.ChatFormatting
import net.minecraft.core.HolderLookup
import net.minecraft.core.HolderLookup.Provider
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.chat.Component
import net.minecraft.network.codec.StreamCodec
import net.minecraft.network.protocol.common.custom.CustomPacketPayload
import net.minecraft.util.datafix.DataFixTypes
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Rarity
import net.minecraft.world.item.TooltipFlag
import net.minecraft.world.level.saveddata.SavedData
import net.neoforged.neoforge.capabilities.Capabilities
import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent
import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent
import net.neoforged.neoforge.event.tick.ServerTickEvent
import net.neoforged.neoforge.network.PacketDistributor
import net.neoforged.neoforge.network.handling.IPayloadContext
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.THREAD_LOCAL_RANDOM
import ru.dbotthepony.mc.otm.capability.FlowDirection
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.trackedItems
import ru.dbotthepony.mc.otm.capability.energy.IMatteryEnergyStorage
import ru.dbotthepony.mc.otm.capability.energy.getBarColor
import ru.dbotthepony.mc.otm.capability.energy.getBarWidth
import ru.dbotthepony.mc.otm.capability.matteryEnergy
import ru.dbotthepony.mc.otm.config.EnergyBalanceValues
import ru.dbotthepony.mc.otm.core.ResourceLocation
import ru.dbotthepony.mc.otm.core.TranslatableComponent
import ru.dbotthepony.mc.otm.core.collect.filter
import ru.dbotthepony.mc.otm.core.isNotEmpty
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.getDecimal
import ru.dbotthepony.mc.otm.core.math.readDecimal
import ru.dbotthepony.mc.otm.core.math.set
import ru.dbotthepony.mc.otm.core.math.writeDecimal
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.nextUUID
import ru.dbotthepony.mc.otm.core.util.formatPower
import ru.dbotthepony.mc.otm.isClientThread
import ru.dbotthepony.mc.otm.isServerThread
import ru.dbotthepony.mc.otm.lazyPerServer
import ru.dbotthepony.mc.otm.registry.CapabilitiesRegisterListener
import ru.dbotthepony.mc.otm.registry.game.MDataComponentTypes
import java.util.*
import java.util.function.Function
import kotlin.collections.MutableList
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.forEach
import kotlin.collections.iterator
import kotlin.collections.set

class QuantumBatteryItem(val savedataID: String, val balanceValues: EnergyBalanceValues?) : Item(Properties().stacksTo(1).rarity(if (balanceValues == null) Rarity.EPIC else Rarity.UNCOMMON)), IQuantumLinked, CapabilitiesRegisterListener {
	val isCreative = balanceValues == null

	interface IValues {
		val uuid: UUID
		var energy: Decimal
		var passed: Decimal
		var received: Decimal
		val isServer: Boolean

		fun serialize(): CompoundTag {
			return CompoundTag().also {
				it["energy"] = energy
				it["passed"] = passed
				it["received"] = received
			}
		}

		fun deserialize(nbt: CompoundTag) {
			energy = nbt.getDecimal("energy")
			passed = nbt.getDecimal("passed")
			received = nbt.getDecimal("received")
		}
	}

	class UnboundValues(override val uuid: UUID = THREAD_LOCAL_RANDOM.nextUUID()) : IValues {
		override var energy: Decimal = Decimal.ZERO
		override var passed: Decimal = Decimal.ZERO
		override var received: Decimal = Decimal.ZERO
		override val isServer: Boolean
			get() = false
	}

	class Data() : SavedData() {
		constructor(nbt: CompoundTag, registry: Provider) : this() {
			load(nbt)
		}

		inner class Values(override val uuid: UUID) : IValues {
			override var energy = Decimal.ZERO
				set(value) {
					field = value
					isDirty = true
				}

			override var passed = Decimal.ZERO
				set(value) {
					field = value
					isDirty = true
				}

			override var received = Decimal.ZERO
				set(value) {
					field = value
					isDirty = true
				}

			override val isServer: Boolean
				get() = true
		}

		private val data = Object2ObjectOpenHashMap<UUID, Values>()

		fun values(uuid: UUID): Values {
			return data.computeIfAbsent(uuid, Function { Values(uuid) })
		}

		fun values(): Values {
			return values(THREAD_LOCAL_RANDOM.nextUUID())
		}

		override fun save(nbt: CompoundTag, registry: HolderLookup.Provider): CompoundTag {
			for ((key, values) in data) {
				nbt[key.toString()] = values.serialize()
			}

			return nbt
		}

		fun load(nbt: CompoundTag) {
			data.clear()

			for (key in nbt.allKeys) {
				val id = UUID.fromString(key)
				data[id] = Values(id).also { it.deserialize(nbt.getCompound(key)) }
			}
		}
	}

	val clientData = Object2ObjectOpenHashMap<UUID, IValues>()

	val serverData: Data by lazyPerServer {
		it.overworld().dataStorage.computeIfAbsent(SavedData.Factory(::Data, ::Data, DataFixTypes.SAVED_DATA_MAP_DATA), "otm_$savedataID")
	}

	private inner class Power(private val stack: ItemStack) : IMatteryEnergyStorage {
		override val energyFlow: FlowDirection
			get() = FlowDirection.BI_DIRECTIONAL

		var values: IValues = UnboundValues()

		fun updateValues() {
			if (!values.isServer && isServerThread()) {
				values = serverData.values(stack[MDataComponentTypes.QUANTUM_LINK_ID] ?: THREAD_LOCAL_RANDOM.nextUUID().also { stack[MDataComponentTypes.QUANTUM_LINK_ID] = it })
			} else if (isClientThread()) {
				val id = stack[MDataComponentTypes.QUANTUM_LINK_ID] ?: return

				if (values.uuid != id)
					values = clientData.computeIfAbsent(id, Function { UnboundValues(it) })
			}
		}

		override fun extractEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
			if (!howMuch.isPositive)
				return Decimal.ZERO

			updateValues()

			if (!values.isServer && !simulate)
				return Decimal.ZERO

			if (balanceValues == null) {
				val newEnergy = (values.energy - howMuch).moreThanZero()
				val diff = values.energy - newEnergy

				if (!simulate) {
					values.energy = newEnergy
					values.passed += diff
				}

				return diff
			} else {
				val newEnergy = (values.energy - howMuch.coerceAtMost(balanceValues.energyThroughput)).moreThanZero()
				val diff = values.energy - newEnergy

				if (!simulate) {
					values.energy = newEnergy
					values.passed += diff
				}

				return diff
			}
		}

		override fun receiveEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
			if (!howMuch.isPositive)
				return Decimal.ZERO

			updateValues()

			if (!values.isServer && !simulate)
				return Decimal.ZERO

			if (balanceValues == null) {
				if (!simulate) {
					values.energy += howMuch
					values.received += howMuch
				}

				return howMuch
			} else if (values.energy >= balanceValues.energyCapacity) {
				return Decimal.ZERO
			} else {
				val newEnergy = (values.energy + howMuch.coerceAtMost(balanceValues.energyThroughput)).coerceAtMost(balanceValues.energyCapacity)
				val diff = newEnergy - values.energy

				if (!simulate) {
					values.energy = newEnergy
					values.received += diff
				}

				return diff
			}
		}

		override var batteryLevel: Decimal
			get() {
				updateValues()
				return values.energy
			}
			set(value) {
				updateValues()
				values.energy = value
			}

		val passed: Decimal get() {
			updateValues()
			return values.passed
		}

		val received: Decimal get() {
			updateValues()
			return values.received
		}

		override val maxBatteryLevel: Decimal
			get() = balanceValues?.energyCapacity ?: (batteryLevel + Decimal.LONG_MAX_VALUE)

		override val missingPower: Decimal
			get() = if (isCreative) Decimal.LONG_MAX_VALUE else super.missingPower
	}

	override fun merge(from: ItemStack, into: ItemStack): ItemStack {
		return from.copyWithCount(from.count + into.count)
	}

	override fun isBarVisible(p_150899_: ItemStack): Boolean {
		if (isCreative) return false
		return p_150899_.matteryEnergy != null
	}

	override fun getBarWidth(p_150900_: ItemStack): Int {
		if (isCreative) return 13
		return p_150900_.matteryEnergy?.getBarWidth() ?: super.getBarWidth(p_150900_)
	}

	override fun getBarColor(p_150901_: ItemStack): Int {
		if (isCreative) return 0
		return p_150901_.matteryEnergy?.getBarColor() ?: super.getBarColor(p_150901_)
	}

	override fun registerCapabilities(event: RegisterCapabilitiesEvent) {
		event.registerItem(Capabilities.EnergyStorage.ITEM, { o, _ -> Power(o) }, this)
		event.registerItem(MatteryCapability.ITEM_ENERGY, { o, _ -> Power(o) }, this)
	}

	override fun appendHoverText(
		itemStack: ItemStack,
		context: TooltipContext,
		components: MutableList<Component>,
		flags: TooltipFlag
	) {
		super.appendHoverText(itemStack, context, components, flags)

		val power = itemStack.getCapability(MatteryCapability.ITEM_ENERGY) as Power
		power.updateValues()

		components.add(TranslatableComponent("otm.item.quantum_description").withStyle(ChatFormatting.DARK_GRAY))

		if (balanceValues == null) {
			components.add(TranslatableComponent("otm.item.quantum_battery.creative_power", power.batteryLevel.formatPower()).withStyle(ChatFormatting.GRAY))
		} else {
			components.add(TranslatableComponent("otm.item.power.storage", power.batteryLevel.formatPower(), balanceValues.energyCapacity.formatPower()).withStyle(ChatFormatting.GRAY))
			components.add(TranslatableComponent("otm.item.power.throughput_mono", balanceValues.energyThroughput.formatPower()).withStyle(ChatFormatting.GRAY))
		}

		components.add(TranslatableComponent("otm.item.power.passed", power.passed.formatPower()).withStyle(ChatFormatting.GRAY))
		components.add(TranslatableComponent("otm.item.power.received", power.received.formatPower()).withStyle(ChatFormatting.GRAY))

		if (isCreative) {
			components.add(TranslatableComponent("otm.item.quantum_battery.creative").withStyle(ChatFormatting.DARK_GRAY))
			components.add(TranslatableComponent("otm.item.quantum_battery.creative2").withStyle(ChatFormatting.DARK_GRAY))
		}

		components.add(TranslatableComponent("otm.item.quantum_link_id", power.values.uuid.toString()).withStyle(ChatFormatting.DARK_GRAY))
	}

	companion object {
		fun clientDisconnect(event: ClientPlayerNetworkEvent.LoggingOut) {
			BuiltInRegistries.ITEM.forEach { if (it is QuantumBatteryItem) it.clientData.clear() }
		}

		fun readPacket(buff: FriendlyByteBuf): ChargePacket {
			return ChargePacket(
				BuiltInRegistries.ITEM.byId(buff.readInt()) as QuantumBatteryItem,
				buff.readUUID(),
				buff.readDecimal(),
				buff.readDecimal(),
				buff.readDecimal(),
			)
		}

		fun tick(event: ServerTickEvent.Post) {
			for (ply in event.server.playerList.players) {
				val networkedChannels = ObjectOpenHashSet<UUID>(0)

				for (item in ply.trackedItems().filter { it.isNotEmpty && it.item is QuantumBatteryItem }) {
					val power = item.getCapability(MatteryCapability.ITEM_ENERGY) as Power
					power.updateValues()

					if (power.values.isServer && networkedChannels.add(power.values.uuid)) {
						PacketDistributor.sendToPlayer(ply, ChargePacket(item.item as QuantumBatteryItem, power.values.uuid, power.batteryLevel, power.passed, power.received))
					}
				}
			}
		}

		val PACKET_TYPE = CustomPacketPayload.Type<ChargePacket>(ResourceLocation(OverdriveThatMatters.MOD_ID, "quantum_charge"))
		val PACKET_CODEC: StreamCodec<FriendlyByteBuf, ChargePacket> = StreamCodec.ofMember(ChargePacket::write, ::readPacket)
	}

	data class ChargePacket(
		val type: QuantumBatteryItem,
		override val uuid: UUID,
		override var energy: Decimal,
		override var passed: Decimal,
		override var received: Decimal,
	) : CustomPacketPayload, IValues {
		override val isServer: Boolean
			get() = false

		override fun type(): CustomPacketPayload.Type<out CustomPacketPayload> {
			return PACKET_TYPE
		}

		fun write(buff: FriendlyByteBuf) {
			buff.writeInt(BuiltInRegistries.ITEM.getId(type))
			buff.writeUUID(uuid)
			buff.writeDecimal(energy)
			buff.writeDecimal(passed)
			buff.writeDecimal(received)
		}

		fun play(context: IPayloadContext) {
			val data = type.clientData.computeIfAbsent(uuid, Function { UnboundValues(it) })
			data.energy = energy
			data.passed = passed
			data.received = received
		}
	}
}
