package ru.dbotthepony.mc.otm.item

import it.unimi.dsi.fastutil.ints.Int2ObjectFunction
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
import it.unimi.dsi.fastutil.ints.IntOpenHashSet
import net.minecraft.ChatFormatting
import net.minecraft.core.Direction
import net.minecraft.nbt.ByteArrayTag
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.Tag
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.chat.Component
import net.minecraft.world.item.*
import net.minecraft.world.level.Level
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.network.NetworkEvent
import net.minecraftforge.registries.ForgeRegistries
import net.minecraftforge.registries.ForgeRegistry
import ru.dbotthepony.mc.otm.*
import ru.dbotthepony.mc.otm.capability.*
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.compat.mekanism.Mattery2MekanismEnergyWrapper
import ru.dbotthepony.mc.otm.config.ConciseBalanceValues
import ru.dbotthepony.mc.otm.core.*
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.readDecimal
import ru.dbotthepony.mc.otm.core.math.writeDecimal
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.util.formatPower
import ru.dbotthepony.mc.otm.network.GenericNetworkChannel
import ru.dbotthepony.mc.otm.network.MatteryPacket
import ru.dbotthepony.mc.otm.network.packetHandled
import ru.dbotthepony.mc.otm.saveddata.SavedCountingMap
import java.util.function.Supplier

class QuantumBatteryItem : Item {
	class Data(
		val parent: SavedCountingMap<Data>?,
		val index: Int = -1,
		energy: Decimal = Decimal.ZERO,
		passed: Decimal = Decimal.ZERO,
		received: Decimal = Decimal.ZERO,
	) {
		constructor(
			energy: Decimal = Decimal.ZERO,
			passed: Decimal = Decimal.ZERO,
			received: Decimal = Decimal.ZERO,
		) : this(null, -1, energy, passed, received)

		var energy: Decimal = energy
			set(value) {
				if (field != value) {
					field = value
					parent?.isDirty = true
				}
			}

		var passed: Decimal = passed
			set(value) {
				if (field != value) {
					field = value
					parent?.isDirty = true
				}
			}

		var received: Decimal = received
			set(value) {
				if (field != value) {
					field = value
					parent?.isDirty = true
				}
			}
	}

	private inner class Power(private val stack: ItemStack) : IMatteryEnergyStorage, ICapabilityProvider {
		private val resolver = LazyOptional.of { this }
		private val resolverMekanism = if (isMekanismLoaded) LazyOptional.of { Mattery2MekanismEnergyWrapper(this) } else null

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

		var data = Data()

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

			return LazyOptional.empty()
		}

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

			if (data.parent == null && isServerThread()) {
				determineQuantumLink()

				if (data.parent == null) {
					return Decimal.ZERO
				}
			}

			if (isCreative) {
				val newEnergy = (data.energy - howMuch).moreThanZero()
				val diff = data.energy - newEnergy

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

				return diff
			}

			val newEnergy = (data.energy - howMuch.coerceAtMost(throughput!!)).moreThanZero()
			val diff = data.energy - newEnergy

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

			return diff
		}

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

			if (data.parent == null && isServerThread()) {
				determineQuantumLink()

				if (data.parent == null) {
					return Decimal.ZERO
				}
			}

			if (isCreative) {
				if (!simulate) {
					data.energy += howMuch
					data.received += howMuch
				}

				return howMuch
			}

			if (data.energy >= capacity!!) {
				return Decimal.ZERO
			}

			val newEnergy = (data.energy + howMuch.coerceAtMost(throughput!!)).coerceAtMost(capacity!!)
			val diff = newEnergy - data.energy

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

			return diff
		}

		override var batteryLevel: Decimal
			get() {
				if (data.parent == null) {
					determineQuantumLink()
				}

				if (isClientThread()) {
					return clientPowerMap[data.index]?.energy ?: data.energy
				}

				return data.energy
			}
			set(value) {
				if (data.parent == null) {
					determineQuantumLink()
				}

				if (isClientThread()) {
					val energy1 = clientPowerMap[data.index]

					if (energy1 != null) {
						energy1.energy = value
					} else {
						data.energy = value
					}

					return
				}

				data.energy = value
			}

		val passed: Decimal
			get() {
			if (data.parent == null) {
				determineQuantumLink()
			}

			if (isClientThread()) {
				return clientPowerMap[data.index]?.passed ?: data.passed
			}

			return data.passed
		}

		val received: Decimal
			get() {
			if (data.parent == null) {
				determineQuantumLink()
			}

			if (isClientThread()) {
				return clientPowerMap[data.index]?.received ?: data.received
			}

			return data.received
		}

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

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

		private fun determineQuantumLink() {
			if (data.parent == null && isServerThread()) {
				val existing = stack.tag?.getInt("link_id")

				if (existing == null) {
					val old = data
					data = saveData!!.factorize()
					data.energy = old.energy
					stack.tagNotNull["link_id"] = data.index
				} else {
					data = saveData?.computeIfAbsent(existing) ?: Data(null, existing, data.energy, data.passed, data.received)
				}
			} else if (!isServerThread()) {
				// client ?
				val existing = stack.tag?.getInt("link_id") ?: return

				if (existing != data.index) {
					data = Data(data.parent, existing, data.energy, data.passed, data.received)
				}
			}
		}

		fun determineQuantumLinkWeak() {
			if (data.parent == null && isServerThread()) {
				val existing = stack.tag?.getInt("link_id")

				if (existing != null) {
					data = saveData?.computeIfAbsent(existing) ?: Data(null, existing, data.energy, data.passed, data.received)
				}
			} else if (!isServerThread()) {
				val existing = stack.tag?.getInt("link_id") ?: return

				if (existing != data.index) {
					data = Data(data.parent, existing, data.energy, data.passed, data.received)
				}
			}
		}
	}

	val isCreative: Boolean

	private val _capacity: () -> Decimal?
	private val _throughput: () -> Decimal?

	val capacity get() = _capacity.invoke()
	val throughput get() = _throughput.invoke()

	val saveDataID: String

	val saveData: SavedCountingMap<Data>? get() {
		if (isServerThread()) {
			return MINECRAFT_SERVER.overworld().dataStorage.computeIfAbsent({
				SavedCountingMap(Companion::storeValue, Companion::loadValue, ::Data).load(it)
			}, {
				SavedCountingMap(Companion::storeValue, Companion::loadValue, ::Data)
			}, saveDataID) ?: throw NullPointerException("Unable to get save data for $this in ${MINECRAFT_SERVER.overworld()}")
		}

		return null
	}

	data class ClientData(
		var energy: Decimal = Decimal.ZERO,
		var passed: Decimal = Decimal.ZERO,
		var received: Decimal = Decimal.ZERO,
	)

	val clientPowerMap: Int2ObjectOpenHashMap<ClientData> by lazy {
		check(isClient) { "Invalid side" }
		Int2ObjectOpenHashMap()
	}

	constructor(saveDataID: String) : super(Properties().stacksTo(1).rarity(Rarity.EPIC)) {
		isCreative = true
		_capacity = { null }
		_throughput = { null }
		this.saveDataID = "otm_$saveDataID".intern()
	}

	constructor(saveDataID: String, capacity: Decimal, io: Decimal) : super(Properties().stacksTo(1)) {
		isCreative = false
		_capacity = { capacity }
		_throughput = { io }
		this.saveDataID = "otm_$saveDataID".intern()
	}

	constructor(saveDataID: String, values: ConciseBalanceValues) : super(Properties().stacksTo(1)) {
		isCreative = false
		_capacity = values::capacity
		_throughput = values::throughput
		this.saveDataID = "otm_$saveDataID".intern()
	}

	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,
		p_41422_: Level?,
		components: MutableList<Component>,
		p_41424_: TooltipFlag
	) {
		super.appendHoverText(itemStack, p_41422_, components, p_41424_)

		val power = itemStack.getCapability(MatteryCapability.ENERGY).orThrow() as Power

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

		if (isCreative) {
			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(), capacity!!.formatPower()).withStyle(ChatFormatting.GRAY))

			components.add(
				TranslatableComponent(
				"otm.item.power.throughput",
				throughput!!.formatPower(),
				throughput!!.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.data.index).withStyle(ChatFormatting.DARK_GRAY))
	}

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

		fun readPacket(buff: FriendlyByteBuf): ChargePacket {
			return ChargePacket(
				(ForgeRegistries.ITEMS as ForgeRegistry<Item>).getValue(buff.readInt()) as QuantumBatteryItem,
				buff.readInt(),
				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 = IntOpenHashSet(0)

					for (item in ply.allItemsStream().filter { !it.isEmpty && it.item is QuantumBatteryItem }) {
						val power = item.getCapability(MatteryCapability.ENERGY).orThrow() as Power

						power.determineQuantumLinkWeak()

						if (power.data.index < 0) {
							continue
						}

						if (networkedChannels.add(power.data.index)) {
							GenericNetworkChannel.send(ply, ChargePacket(item.item as QuantumBatteryItem, power.data.index, power.batteryLevel, power.data.passed, power.data.received))
						}
					}
				}
			}
		}

		private fun loadValue(parent: SavedCountingMap<Data>, tag: Tag, index: Int): Data {
			if (tag is ByteArrayTag) {
				return Data(parent, index, Decimal.deserializeNBT(tag))
			} else if (tag is CompoundTag) {
				return Data(parent, index, Decimal.deserializeNBT(tag["energy"]), Decimal.deserializeNBT(tag["passed"]), Decimal.deserializeNBT(tag["received"]))
			} else {
				return Data(parent, index)
			}
		}

		private fun storeValue(parent: SavedCountingMap<Data>, value: Data, index: Int): CompoundTag {
			return CompoundTag().also {
				it["energy"] = value.energy.serializeNBT()
				it["passed"] = value.passed.serializeNBT()
				it["received"] = value.received.serializeNBT()
			}
		}
	}

	class ChargePacket(
		val type: QuantumBatteryItem,
		val channel: Int,
		val energy: Decimal,
		val passed: Decimal,
		val received: Decimal,
	) : MatteryPacket {
		override fun write(buff: FriendlyByteBuf) {
			buff.writeInt((ForgeRegistries.ITEMS as ForgeRegistry<Item>).getID(type))
			buff.writeInt(channel)
			buff.writeDecimal(energy)
			buff.writeDecimal(passed)
			buff.writeDecimal(received)
		}

		override fun play(context: Supplier<NetworkEvent.Context>) {
			context.packetHandled = true
			val data = type.clientPowerMap.computeIfAbsent(channel, Int2ObjectFunction { ClientData() })
			data.energy = energy
			data.passed = passed
			data.received = received
		}
	}
}
