package ru.dbotthepony.mc.otm.menu.matter

import net.minecraft.network.FriendlyByteBuf
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.Item
import net.minecraftforge.network.NetworkEvent
import net.minecraftforge.network.PacketDistributor
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.block.entity.matter.MatterPanelBlockEntity
import ru.dbotthepony.mc.otm.capability.matter.*
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.core.GetterSetter
import ru.dbotthepony.mc.otm.core.addSorted
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.util.BooleanValueCodec
import ru.dbotthepony.mc.otm.core.util.DecimalValueCodec
import ru.dbotthepony.mc.otm.core.util.ItemSorter
import ru.dbotthepony.mc.otm.core.util.codec
import ru.dbotthepony.mc.otm.graph.matter.IMatterGraphListener
import ru.dbotthepony.mc.otm.graph.matter.MatterGraph
import ru.dbotthepony.mc.otm.menu.MatteryMenu
import ru.dbotthepony.mc.otm.network.*
import ru.dbotthepony.mc.otm.registry.MMenus
import java.util.*
import java.util.function.Predicate
import java.util.function.Supplier
import kotlin.Comparator
import kotlin.collections.ArrayList

class CancelTaskPacket(val id: UUID) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeUUID(id)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.enqueueWork {
			val sender = context.sender!!
			(sender.containerMenu as? MatterPanelMenu)?.receiveTaskCancel(sender, id)
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf): CancelTaskPacket {
			return CancelTaskPacket(buff.readUUID())
		}
	}
}

class PatternsChangePacket(val isUpdate: Boolean, val patterns: Collection<IPatternState>) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeBoolean(isUpdate)
		buff.writeInt(patterns.size)

		for (pattern in patterns) {
			pattern.write(buff)
		}
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.enqueueWork {
			val menu = minecraft.player?.containerMenu as? MatterPanelMenu ?: return@enqueueWork

			if (isUpdate) {
				menu.networkPatternsUpdated(patterns)
			} else {
				menu.networkPatternsRemoved(patterns)
			}
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf): PatternsChangePacket {
			val isUpdate = buff.readBoolean()
			val size = buff.readInt()

			return PatternsChangePacket(isUpdate, ArrayList<PatternState>(size).also {
				for (i in 0 until size) it.add(PatternState.read(buff) ?: throw NullPointerException("Unable to read PatternState from network at $i")) })
		}
	}
}

class TasksChangePacket(val isUpdate: Boolean, val tasks: Collection<IReplicationTask<*>>) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeBoolean(isUpdate)
		buff.writeInt(tasks.size)

		for (task in tasks) {
			task.write(buff)
		}
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.enqueueWork {
			val menu = minecraft.player?.containerMenu as? MatterPanelMenu ?: return@enqueueWork

			if (isUpdate) {
				menu.networkTasksUpdated(tasks)
			} else {
				menu.networkTasksRemoved(tasks)
			}
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf): TasksChangePacket {
			val isUpdate = buff.readBoolean()
			val size = buff.readInt()

			return TasksChangePacket(isUpdate, ArrayList<IReplicationTask<*>>(size).also {
				for (i in 0 until size) it.add(buff.readReplicationTask() ?: throw NullPointerException("Unable to read Replication Task from network at $i")) })
		}
	}
}

class ReplicationRequestPacket(val id: UUID, amount: Int) : MatteryPacket {
	val amount = amount.coerceAtLeast(1).coerceAtMost(99999)

	override fun write(buff: FriendlyByteBuf) {
		buff.writeUUID(id)
		buff.writeInt(amount)
	}

	override fun play(context: Supplier<NetworkEvent.Context>) {
		context.packetHandled = true
		context.enqueueWork {
			val sender = context.sender ?: return@enqueueWork
			val menu = sender.containerMenu as? MatterPanelMenu ?: return@enqueueWork

			menu.requestReplication(sender, id, amount)
		}
	}

	companion object {
		fun read(buff: FriendlyByteBuf): ReplicationRequestPacket {
			return ReplicationRequestPacket(buff.readUUID(), buff.readInt())
		}
	}
}

class MatterPanelMenu @JvmOverloads constructor(
	p_38852_: Int,
	inventory: Inventory,
	tile: MatterPanelBlockEntity? = null
) : MatteryMenu(MMenus.MATTER_PANEL, p_38852_, inventory, tile), IMatterGraphListener {
	fun taskUpdated(task: IReplicationTask<*>) {
		sendNetwork(TasksChangePacket(true, listOf(task)))
	}

	fun taskRemoved(task: IReplicationTask<*>) {
		sendNetwork(TasksChangePacket(false, listOf(task)))
	}

	val sorting: ItemSorter by mSynchronizer.ComputedField(
		getter = { tile?.getPlayerSettings(ply)?.sorter ?: ItemSorter.DEFAULT },
		codec = ItemSorter::class.codec(),
		observer = {
			patterns.sortWith(actualComparator)
			patternsFiltered.sortWith(actualComparator)
			tasks.sortWith(actualTaskComparator)
			tasksFiltered.sortWith(actualTaskComparator)
		})

	val isAscending: Boolean by mSynchronizer.ComputedField(
		getter = { tile?.getPlayerSettings(ply)?.ascending ?: true },
		codec = BooleanValueCodec,
		observer = {
			patterns.sortWith(actualComparator)
			patternsFiltered.sortWith(actualComparator)
			tasks.sortWith(actualTaskComparator)
			tasksFiltered.sortWith(actualTaskComparator)
		})

	val totalMatterStored: Decimal by mSynchronizer.ComputedField(
		getter = { tile?.matterNode?.graph?.getMatterStorageLevel() ?: Decimal.ZERO },
		codec = DecimalValueCodec,
	)

	var filter: Predicate<Item> = Predicate { true }
		set(value) {
			field = value
			patternsFiltered.clear()
			tasksFiltered.clear()

			for (pattern in patterns) {
				if (value.test(pattern.item)) {
					patternsFiltered.add(pattern)
				}
			}

			for (task in tasks) {
				if (value.test(task.item)) {
					tasksFiltered.add(task)
				}
			}
		}

	val changeIsAscending = booleanInput(allowSpectators = true) { tile?.getPlayerSettings(ply)?.ascending = it }
	val changeSorting = PlayerInput(ItemSorter::class.codec(), allowSpectators = true) { tile?.getPlayerSettings(ply)?.sorter = it }

	val sortingGS = GetterSetter.of(::sorting, changeSorting::input)
	val isAscendingGS = GetterSetter.of(::isAscending, changeIsAscending::input)

	private val actualComparator = Comparator<IPatternState> { o1, o2 -> sorting.comparator.compare(o1.item, o2.item) * (if (isAscending) 1 else -1) }
	private val actualTaskComparator = Comparator<IReplicationTask<*>> { o1, o2 -> sorting.comparator.compare(o1.item, o2.item) * (if (isAscending) 1 else -1) }

	private val patterns = ArrayList<IPatternState>()
	private val tasks = ArrayList<IReplicationTask<*>>()

	val patternsFiltered = ArrayList<IPatternState>()
	val tasksFiltered = ArrayList<IReplicationTask<*>>()

	fun networkPatternsUpdated(patterns: Collection<IPatternState>) {
		for (pattern in patterns) {
			val index = this.patterns.indexOfFirst(pattern::matchId)

			if (index != -1) {
				this.patterns[index] = pattern
			} else {
				this.patterns.addSorted(pattern, actualComparator)
			}

			if (filter.test(pattern.item)) {
				val index = this.patternsFiltered.indexOfFirst(pattern::matchId)

				if (index != -1) {
					this.patternsFiltered[index] = pattern
				} else {
					this.patternsFiltered.addSorted(pattern, actualComparator)
				}
			}
		}
	}

	fun networkPatternsRemoved(patterns: Collection<IPatternState>) {
		for (pattern in patterns) {
			this.patterns.remove(pattern)
		}
	}

	fun networkTasksUpdated(tasks: Collection<IReplicationTask<*>>) {
		for (task in tasks) {
			val index = this.tasks.indexOfFirst(task::matchId)

			if (index != -1) {
				this.tasks[index] = task
			} else {
				this.tasks.addSorted(task, actualTaskComparator)
			}

			if (filter.test(task.item)) {
				val index = this.tasksFiltered.indexOfFirst(task::matchId)

				if (index != -1) {
					this.tasksFiltered[index] = task
				} else {
					this.tasksFiltered.addSorted(task, actualTaskComparator)
				}
			}
		}
	}

	fun networkTasksRemoved(tasks: Collection<IReplicationTask<*>>) {
		for (task in tasks) {
			val index = this.tasks.indexOfFirst(task::matchId)

			if (index != -1) {
				this.tasks.removeAt(index)

				if (filter.test(task.item)) {
					val index = this.tasksFiltered.indexOfFirst(task::matchId)

					if (index != -1) {
						this.tasksFiltered.removeAt(index)
					}
				}
			}
		}
	}

	// server code
	fun requestReplication(ply: ServerPlayer, id: UUID, count: Int) {
		if (ply.isSpectator) {
			return
		}

		val tile = tile as MatterPanelBlockEntity? ?: return

		val state = tile.matterNode.graph.getPattern(id)

		if (state == null) {
			LOGGER.error("Received replication request from {} of {}, but it is not found in grid", ply, id)
			return
		}

		tile.addTask(state, count)
	}

	fun receiveTaskCancel(ply: ServerPlayer, id: UUID) {
		if (!ply.isSpectator)
			(tile as MatterPanelBlockEntity?)?.removeTask(id)
	}

	fun requestTaskCancel(id: UUID) {
		if (minecraft.player?.isSpectator != true)
			MenuNetworkChannel.sendToServer(CancelTaskPacket(id))
	}

	override fun onPatternAdded(state: IPatternState) {
		sendNetwork(PatternsChangePacket(true, listOf(state)))
	}

	override fun onPatternRemoved(state: IPatternState) {
		sendNetwork(PatternsChangePacket(false, listOf(state)))
	}

	override fun onPatternUpdated(newState: IPatternState, oldState: IPatternState) {
		sendNetwork(PatternsChangePacket(true, listOf(newState)))
	}

	private var initialSend = false
	private var listeningGrid: MatterGraph? = null

	init {
		if (tile != null) {
			listeningGrid = tile.matterNode.graph
			tile.attachMenu(this)
			listeningGrid!!.addListener(this)
		}
	}

	override fun removed(p_38940_: Player) {
		super.removed(p_38940_)
		(tile as MatterPanelBlockEntity?)?.deatachMenu(this)
		listeningGrid?.removeListener(this)
	}

	override fun broadcastChanges() {
		super.broadcastChanges()
		if (!initialSend) fullPatternBroadcast()
	}

	private fun sendNetwork(packet: Any) {
		MenuNetworkChannel.send(PacketDistributor.PLAYER.with { inventory.player as ServerPlayer }, packet)
	}

	fun fullPatternBroadcast() {
		if (inventory.player.level().isClientSide) {
			initialSend = true
			return
		}

		val tile = tile as MatterPanelBlockEntity?

		if (tile != null) {
			val grid = tile.matterNode.graph
			initialSend = true
			sendNetwork(PatternsChangePacket(true, grid.patterns.toList()))
			sendNetwork(TasksChangePacket(true, tile.allReplicationTasks.toList()))
		}
	}

	companion object {
		private val LOGGER = LogManager.getLogger()
	}
}
