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 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.addSorted
import ru.dbotthepony.mc.otm.core.map
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.util.DecimalValueCodec
import ru.dbotthepony.mc.otm.core.util.NullValueCodec
import ru.dbotthepony.mc.otm.core.util.writeCollection
import ru.dbotthepony.mc.otm.graph.matter.IMatterGraphListener
import ru.dbotthepony.mc.otm.graph.matter.MatterGraph
import ru.dbotthepony.mc.otm.menu.IItemSortingSettings
import ru.dbotthepony.mc.otm.menu.MatteryMenu
import ru.dbotthepony.mc.otm.menu.input.BooleanInputWithFeedback
import ru.dbotthepony.mc.otm.network.*
import ru.dbotthepony.mc.otm.registry.MMenus
import java.util.*
import java.util.function.Predicate

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

	override fun play(context: MNetworkContext) {
		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<PatternState>) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeBoolean(isUpdate)
		buff.writeCollection(patterns, PatternState::write)
	}

	override fun play(context: MNetworkContext) {
		val menu = minecraft.player?.containerMenu as? MatterPanelMenu ?: return

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

	companion object {
		fun read(buff: FriendlyByteBuf): PatternsChangePacket {
			return PatternsChangePacket(buff.readBoolean(), buff.readCollection(::ArrayList, PatternState::read))
		}
	}
}

class TasksChangePacket(val isUpdate: Boolean, val tasks: Collection<ReplicationTask>) : MatteryPacket {
	override fun write(buff: FriendlyByteBuf) {
		buff.writeBoolean(isUpdate)
		buff.writeCollection(tasks, ReplicationTask::write)
	}

	override fun play(context: MNetworkContext) {
		val menu = minecraft.player?.containerMenu as? MatterPanelMenu ?: return

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

	companion object {
		fun read(buff: FriendlyByteBuf): TasksChangePacket {
			return TasksChangePacket(buff.readBoolean(), buff.readCollection(::ArrayList, ReplicationTask::read))
		}
	}
}

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: MNetworkContext) {
		val sender = context.sender ?: return
		val menu = sender.containerMenu as? MatterPanelMenu ?: return

		menu.requestReplication(sender, id, amount)
	}

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

class MatterPanelMenu(
	containerId: Int,
	inventory: Inventory,
	tile: MatterPanelBlockEntity? = null
) : MatteryMenu(MMenus.MATTER_PANEL, containerId, inventory, tile), IMatterGraphListener {
	fun taskUpdated(task: ReplicationTask) {
		sendNetwork(TasksChangePacket(true, listOf(task)))
	}

	fun taskRemoved(task: ReplicationTask) {
		sendNetwork(TasksChangePacket(false, listOf(task)))
	}

	private fun updateComparators() {
		var p = settings.sorting.map(PatternState::item)
		var t = settings.sorting.map(ReplicationTask::item)

		if (!settings.isAscending) {
			p = p.reversed()
			t = t.reversed()
		}

		patternComparator = p
		taskComparator = t
	}

	val settings = IItemSortingSettings.inputs(this, tile?.getPlayerSettings(player), ::updateComparators)

	var isProvidingTasks by BooleanInputWithFeedback(this, tile?.let { it::isProvidingTasks })
	val cancelAll = PlayerInput(NullValueCodec) { tile?.dropAllTasks() }

	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)
				}
			}
		}

	private var patternComparator = settings.sorting.map(PatternState::item)
		set(value) {
			if (value != field) {
				field = value
				patterns.sortWith(value)
				patternsFiltered.sortWith(value)
			}
		}

	private var taskComparator = settings.sorting.map(ReplicationTask::item)
		set(value) {
			if (value != field) {
				field = value
				tasks.sortWith(value)
				tasksFiltered.sortWith(value)
			}
		}

	private val patterns = ArrayList<PatternState>()
	private val tasks = ArrayList<ReplicationTask>()

	val patternsFiltered = ArrayList<PatternState>()
	val tasksFiltered = ArrayList<ReplicationTask>()

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

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

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

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

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

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

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

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

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

	fun networkTasksRemoved(tasks: Collection<ReplicationTask>) {
		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: PatternState) {
		sendNetwork(PatternsChangePacket(true, listOf(state)))
	}

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

	override fun onPatternUpdated(newState: PatternState, oldState: PatternState) {
		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?)?.detachMenu(this)
		listeningGrid?.removeListener(this)
	}

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

	private fun sendNetwork(packet: Any) {
		MenuNetworkChannel.send(inventory.player, 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()
	}
}
