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.Direction
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.chat.Component
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.Level
import net.minecraft.world.level.saveddata.SavedData
import net.minecraftforge.client.event.ClientPlayerNetworkEvent
import net.minecraftforge.common.capabilities.Capability
import net.minecraftforge.common.capabilities.ForgeCapabilities
import net.minecraftforge.common.capabilities.ICapabilityProvider
import net.minecraftforge.common.util.LazyOptional
import net.minecraftforge.event.TickEvent
import net.minecraftforge.event.TickEvent.ServerTickEvent
import net.minecraftforge.registries.ForgeRegistries
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.TranslatableComponent
import ru.dbotthepony.mc.otm.core.collect.filter
import ru.dbotthepony.mc.otm.core.getID
import ru.dbotthepony.mc.otm.core.getValue
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.getUUIDSafe
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.orThrow
import ru.dbotthepony.mc.otm.core.tagNotNull
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.network.GenericNetworkChannel
import ru.dbotthepony.mc.otm.network.MNetworkContext
import ru.dbotthepony.mc.otm.network.MatteryPacket
import java.util.*
import java.util.function.Function
import java.util.function.Supplier
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 {
	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 = UUID.randomUUID()) : 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) : 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(UUID.randomUUID())
		}

		override fun save(nbt: CompoundTag): 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(::Data, ::Data, "otm_$savedataID")
	}

	private inner class Power(private val stack: ItemStack) : IMatteryEnergyStorage, ICapabilityProvider {
		private val resolver = LazyOptional.of { this }

		override val energyFlow: FlowDirection
			get() = FlowDirection.BI_DIRECTIONAL

		var values: IValues = UnboundValues()

		fun updateValues() {
			if (!values.isServer && isServerThread()) {
				values = serverData.values(stack.tag?.getUUIDSafe("uuid") ?: UUID.randomUUID().also { stack.tagNotNull["uuid"] = it })
			} else if (isClientThread()) {
				val id = stack.tag?.getUUIDSafe("uuid") ?: return

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

		override fun <T : Any?> getCapability(cap: Capability<T>, side: Direction?): LazyOptional<T> {
			if (cap == ForgeCapabilities.ENERGY || cap == MatteryCapability.ENERGY) {
				return resolver.cast()
			}

			return LazyOptional.empty()
		}

		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 initCapabilities(stack: ItemStack, nbt: CompoundTag?): ICapabilityProvider {
		return Power(stack)
	}

	override fun appendHoverText(itemStack: ItemStack, level: Level?, components: MutableList<Component>, flags: TooltipFlag) {
		super.appendHoverText(itemStack, level, components, flags)

		val power = itemStack.getCapability(MatteryCapability.ENERGY).orThrow() 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).withStyle(ChatFormatting.DARK_GRAY))
	}

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

		fun readPacket(buff: FriendlyByteBuf): ChargePacket {
			return ChargePacket(
				ForgeRegistries.ITEMS.getValue(buff.readInt()) as QuantumBatteryItem,
				buff.readUUID(),
				buff.readDecimal(),
				buff.readDecimal(),
				buff.readDecimal(),
			)
		}

		fun tick(event: ServerTickEvent) {
			if (event.phase == TickEvent.Phase.END) {
				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.ENERGY).orThrow() as Power
						power.updateValues()
						if (power.values.isServer && networkedChannels.add(power.values.uuid)) {
							GenericNetworkChannel.send(ply, ChargePacket(item.item as QuantumBatteryItem, power.values.uuid, power.batteryLevel, power.passed, power.received))
						}
					}
				}
			}
		}
	}

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

		override fun write(buff: FriendlyByteBuf) {
			buff.writeInt(ForgeRegistries.ITEMS.getID(type))
			buff.writeUUID(uuid)
			buff.writeDecimal(energy)
			buff.writeDecimal(passed)
			buff.writeDecimal(received)
		}

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