package ru.dbotthepony.mc.otm.data

import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.mojang.datafixers.util.Either
import com.mojang.serialization.Codec
import com.mojang.serialization.JsonOps
import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.ChatFormatting
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.core.registries.Registries
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.codec.StreamCodec
import net.minecraft.network.protocol.common.custom.CustomPacketPayload
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerPlayer
import net.minecraft.server.packs.resources.ResourceManager
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener
import net.minecraft.tags.BlockTags
import net.minecraft.tags.TagKey
import net.minecraft.util.profiling.ProfilerFiller
import net.minecraft.world.item.BlockItem
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.state.BlockState
import net.neoforged.neoforge.event.AddReloadListenerEvent
import net.neoforged.neoforge.event.OnDatapackSyncEvent
import net.neoforged.neoforge.event.entity.player.ItemTooltipEvent
import net.neoforged.neoforge.network.PacketDistributor
import net.neoforged.neoforge.network.handling.IPayloadContext
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.OverdriveThatMatters
import ru.dbotthepony.mc.otm.SERVER_IS_LIVE
import ru.dbotthepony.mc.otm.client.isShiftDown
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.core.ResourceLocation
import ru.dbotthepony.mc.otm.core.TextComponent
import ru.dbotthepony.mc.otm.core.TranslatableComponent
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.readBlockType
import ru.dbotthepony.mc.otm.core.registryName
import ru.dbotthepony.mc.otm.core.util.formatPower
import ru.dbotthepony.mc.otm.core.writeBlockType
import ru.dbotthepony.mc.otm.data.codec.DecimalCodec
import ru.dbotthepony.mc.otm.data.codec.minRange

object FlywheelMaterials : SimpleJsonResourceReloadListener(GsonBuilder().setPrettyPrinting().create(), "otm_flywheel_materials") {
	data class Entry(
		val id: Either<TagKey<Block>, Block>,
		val storage: Decimal,
		val priority: Int = 0,
		val receiveEfficiency: Decimal = Decimal.ONE,
		val momentumLossSpeed: Decimal = Decimal.ONE
	) : Comparable<Entry> {
		constructor(id: Block, storage: Decimal, priority: Int = 0, receiveEfficiency: Decimal = Decimal.ONE, momentumLossSpeed: Decimal = Decimal.ONE) : this(Either.right(id), storage, priority, receiveEfficiency, momentumLossSpeed)
		constructor(id: TagKey<Block>, storage: Decimal, priority: Int = 0, receiveEfficiency: Decimal = Decimal.ONE, momentumLossSpeed: Decimal = Decimal.ONE) : this(Either.left(id), storage, priority, receiveEfficiency, momentumLossSpeed)

		override fun compareTo(other: Entry): Int {
			val selfLeft = id.left().isPresent
			val otherLeft = other.id.left().isPresent

			if (selfLeft && otherLeft || !selfLeft && !otherLeft) {
				val cmp = priority.compareTo(other.priority)

				if (cmp == 0) {
					if (selfLeft) {
						return id.left().get().location.compareTo(other.id.left().get().location)
					} else {
						return id.right().get().registryName!!.compareTo(other.id.right().get().registryName!!)
					}
				}

				return cmp
			} else if (selfLeft) {
				return -1
			} else {
				return 1
			}
		}

		fun write(stream: FriendlyByteBuf) {
			stream.writeBoolean(id.left().isPresent)

			id
				.ifLeft { stream.writeResourceLocation(it.location) }
				.ifRight { stream.writeBlockType(it) }

			stream.writeDecimal(storage)
			stream.writeVarInt(priority)
			stream.writeBoolean(receiveEfficiency != Decimal.ONE)
			if (receiveEfficiency != Decimal.ONE) stream.writeDecimal(receiveEfficiency)
			stream.writeBoolean(momentumLossSpeed != Decimal.ONE)
			if (momentumLossSpeed != Decimal.ONE) stream.writeDecimal(momentumLossSpeed)
		}
	}

	private fun readEntry(stream: FriendlyByteBuf): Entry {
		val id: Either<TagKey<Block>, Block> = if (stream.readBoolean()) {
			Either.left(BlockTags.create(stream.readResourceLocation()))
		} else {
			Either.right(stream.readBlockType())
		}

		val storage = stream.readDecimal()
		val priority = stream.readVarInt()
		val receiveEfficiency = if (stream.readBoolean()) stream.readDecimal() else Decimal.ONE
		val momentumLossSpeed = if (stream.readBoolean()) stream.readDecimal() else Decimal.ONE

		return Entry(id, storage, priority, receiveEfficiency, momentumLossSpeed)
	}

	private var elements = ArrayList<Entry>()
	private var mapped = HashMap<Block, Entry>()
	private var mappingChanged = false

	private fun mapping(): Map<Block, Entry> {
		var mapped = mapped

		if (mappingChanged) {
			mapped = HashMap()

			for (element in elements) {
				element.id
					.ifLeft {
						for (b in BuiltInRegistries.BLOCK.getTagOrEmpty(it)) {
							mapped[b.value()] = element
						}
					}
					.ifRight {
						mapped[it] = element
					}
			}

			this.mapped = mapped
			this.mappingChanged = false
			return mapped
		}

		return mapped
	}

	operator fun get(index: Block): Entry? {
		return mapping()[index]
	}

	operator fun get(index: BlockState): Entry? {
		return this[index.block]
	}

	operator fun contains(value: Block): Boolean {
		return value in mapping()
	}

	operator fun contains(value: BlockState): Boolean {
		return value.block in mapping()
	}

	override fun apply(
		map: MutableMap<ResourceLocation, JsonElement>,
		manager: ResourceManager,
		profiler: ProfilerFiller
	) {
		val elements = ArrayList<Entry>()

		for ((path, element) in map.entries) {
			if (element is JsonObject) {
				CODEC.decode(JsonOps.INSTANCE, element)
					.ifError { LOGGER.error("Unable to deserialize flywheel material entry at $path: $it") }
					.ifSuccess { elements.add(it.first) }
			} else if (element is JsonArray) {
				for ((i, elem) in element.withIndex()) {
					CODEC.decode(JsonOps.INSTANCE, elem)
						.ifError { LOGGER.error("Unable to deserialize flywheel material entry at $path, offset $i: $it") }
						.ifSuccess { elements.add(it.first) }
				}
			}
		}

		elements.sortDescending()

		this.elements = elements
		this.mappingChanged = true
	}

	val CODEC: Codec<Entry> = RecordCodecBuilder.create {
		it.group(
			Codec.either(
				TagKey.hashedCodec(Registries.BLOCK),
				BuiltInRegistries.BLOCK.byNameCodec()
			).fieldOf("id").forGetter(Entry::id),
			DecimalCodec.minRange(Decimal.ONE).fieldOf("storage").forGetter(Entry::storage),
			Codec.INT.optionalFieldOf("priority", 0).forGetter(Entry::priority),
			DecimalCodec.minRange(Decimal.ZERO).optionalFieldOf("receive_efficiency", Decimal.ONE).forGetter(Entry::receiveEfficiency),
			DecimalCodec.minRange(Decimal.ZERO).optionalFieldOf("momentum_loss_speed", Decimal.ONE).forGetter(Entry::momentumLossSpeed),
		).apply(it, ::Entry)
	}

	fun reloadEvent(event: AddReloadListenerEvent) {
		event.addListener(this)
	}

	val SYNC_TYPE = CustomPacketPayload.Type<SyncPacket>(ResourceLocation(OverdriveThatMatters.MOD_ID, "flywheel_materials_sync"))
	val SYNC_CODEC: StreamCodec<FriendlyByteBuf, SyncPacket> = StreamCodec.ofMember(SyncPacket::write, ::readSyncPacket)

	private fun readSyncPacket(buff: FriendlyByteBuf): SyncPacket {
		return SyncPacket(buff.readList { readEntry(it) })
	}

	class SyncPacket(val elements: List<Entry>) : CustomPacketPayload {
		override fun type(): CustomPacketPayload.Type<out CustomPacketPayload> {
			return SYNC_TYPE
		}

		fun write(stream: FriendlyByteBuf) {
			stream.writeCollection(elements) { s, entry -> entry.write(s) }
		}

		fun play(context: IPayloadContext) {
			if (SERVER_IS_LIVE)
				return // singleplayer or LAN host

			FlywheelMaterials.elements = ArrayList(elements)
			mappingChanged = true
		}
	}

	fun onDataPackSync(event: OnDatapackSyncEvent) {
		if (event.player == null) {
			PacketDistributor.sendToAllPlayers(SyncPacket(elements))
		} else {
			PacketDistributor.sendToPlayer(event.player as ServerPlayer, SyncPacket(elements))
		}
	}

	fun tooltipEvent(event: ItemTooltipEvent) {
		if (minecraft.window.isShiftDown) {
			val item = event.itemStack.item

			if (item is BlockItem) {
				val entry = this[item.block]

				if (entry != null) {
					event.toolTip.add(TextComponent(""))
					event.toolTip.add(TranslatableComponent("otm.gui.flywheel.top").withStyle(ChatFormatting.GRAY))
					event.toolTip.add(TranslatableComponent("otm.gui.flywheel.storage", entry.storage.formatPower()).withStyle(ChatFormatting.GRAY))

					if (entry.receiveEfficiency != Decimal.ONE) {
						event.toolTip.add(TranslatableComponent("otm.gui.flywheel.receive_efficiency", entry.receiveEfficiency.toString(2)).withStyle(ChatFormatting.GRAY))
					}

					if (entry.momentumLossSpeed != Decimal.ONE) {
						event.toolTip.add(TranslatableComponent("otm.gui.flywheel.momentum_loss_speed", entry.momentumLossSpeed.toString(2)).withStyle(ChatFormatting.GRAY))
					}
				}
			}
		}
	}

	private val LOGGER = LogManager.getLogger()
}
