package ru.dbotthepony.mc.otm.block.entity.matter

import it.unimi.dsi.fastutil.objects.Object2ObjectFunction
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import net.minecraft.core.BlockPos
import net.minecraft.world.level.block.state.BlockState
import ru.dbotthepony.mc.otm.menu.matter.MatterPanelMenu
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import java.util.HashMap
import java.util.UUID
import net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
import net.minecraft.server.level.ServerLevel
import net.minecraft.world.level.Level
import net.minecraftforge.common.util.INBTSerializable
import ru.dbotthepony.mc.otm.block.entity.MatteryDeviceBlockEntity
import ru.dbotthepony.mc.otm.capability.matter.*
import ru.dbotthepony.mc.otm.core.nbt.getBoolean
import ru.dbotthepony.mc.otm.core.nbt.getCompoundList
import ru.dbotthepony.mc.otm.core.nbt.map
import ru.dbotthepony.mc.otm.core.nbt.mapString
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.util.ItemSorter
import ru.dbotthepony.mc.otm.graph.Graph6Node
import ru.dbotthepony.mc.otm.graph.matter.MatterGraph
import ru.dbotthepony.mc.otm.graph.matter.MatterNode
import ru.dbotthepony.mc.otm.graph.matter.SimpleMatterNode
import ru.dbotthepony.mc.otm.registry.MBlockEntities
import java.util.ArrayList
import java.util.stream.Stream

class MatterPanelBlockEntity(p_155229_: BlockPos, p_155230_: BlockState) :
	MatteryDeviceBlockEntity(MBlockEntities.MATTER_PANEL, p_155229_, p_155230_), IReplicationTaskProvider {

	class PlayerSettings(var sorter: ItemSorter = ItemSorter.DEFAULT, var ascending: Boolean = true) : INBTSerializable<CompoundTag> {
		override fun serializeNBT(): CompoundTag {
			return CompoundTag().also {
				it["sorter"] = sorter.name
				it["ascending"] = ascending
			}
		}

		override fun deserializeNBT(nbt: CompoundTag) {
			sorter = nbt.mapString("sorter", ItemSorter::valueOf, ItemSorter.DEFAULT)
			ascending = nbt.getBoolean("ascending", true)
		}
	}

	private val playerSettings = Object2ObjectOpenHashMap<UUID, PlayerSettings>()

	fun getPlayerSettings(ply: Player): PlayerSettings {
		return playerSettings.computeIfAbsent(ply.uuid, Object2ObjectFunction { PlayerSettings() })
	}

	private val listeners = ArrayList<MatterPanelMenu>()
	val matterNode = SimpleMatterNode(tasks = this)

	fun attachMenu(menu: MatterPanelMenu) {
		listeners.add(menu)
	}

	fun deatachMenu(menu: MatterPanelMenu) {
		listeners.remove(menu)
	}

	override fun createMenu(containerID: Int, inventory: Inventory, ply: Player): AbstractContainerMenu {
		return MatterPanelMenu(containerID, inventory, this)
	}

	init {
		exposeGlobally(MatteryCapability.MATTER_NODE, matterNode)
		exposeGlobally(MatteryCapability.TASK, this)
	}

	override fun setLevel(level: Level) {
		super.setLevel(level)
		matterNode.discover(this)
	}

	override fun setRemoved() {
		super.setRemoved()
		matterNode.isValid = false
	}

	private val _tasks = HashMap<UUID, ReplicationTask>()

	override val replicationTasks: Stream<ReplicationTask> get() {
		return _tasks.values.stream().filter { it.required > 0 }
	}

	override val allReplicationTasks: Stream<ReplicationTask> get() {
		return _tasks.values.stream()
	}

	override fun allocateTask(simulate: Boolean): ReplicationTaskAllocation? {
		val graph = matterNode.graph as MatterGraph? ?: return null

		for ((key, task) in _tasks) {
			if (task.required > 0) {
				val pattern = task.patternId?.let(graph::getPattern) ?: continue

				if (!simulate) {
					val new = task.allocate()
					_tasks[key] = new
					listeners.forEach { it.taskUpdated(new) }
					graph.onMatterTaskUpdated(new, task)
					setChanged()
				}

				return ReplicationTaskAllocation(task, pattern)
			}
		}

		return null
	}

	override fun notifyTaskCompletion(taskId: UUID): Boolean {
		var localTask = _tasks[taskId] ?: return false
		val oldTask = localTask

		localTask = localTask.finish()
		val graph = matterNode.graph as MatterGraph?

		// Задача полностью выполнена
		if (localTask.required <= 0 && localTask.inProgress <= 0) {
			_tasks.remove(taskId)
			graph?.onMatterTaskFinished(localTask)
			listeners.forEach { it.taskRemoved(localTask) }
		} else {
			// Задача обновлена
			_tasks[taskId] = localTask
			graph?.onMatterTaskUpdated(localTask, oldTask)
			listeners.forEach { it.taskUpdated(localTask) }
		}

		setChanged()
		return true
	}

	override fun saveShared(nbt: CompoundTag) {
		super.saveShared(nbt)

		val list = ListTag()

		for (task in _tasks.values) {
			list.add(task.serializeNBT())
		}

		nbt["tasks"] = list

		val settings = CompoundTag()

		for ((uuid, value) in playerSettings) {
			settings[uuid.toString()] = value.serializeNBT()
		}

		nbt["settings"] = settings
	}

	override fun load(nbt: CompoundTag) {
		super.load(nbt)
		_tasks.clear()
		val list = nbt.getCompoundList("tasks")

		for (tag in list) {
			val task = ReplicationTask.deserializeNBT(tag)

			if (task != null) {
				_tasks[task.id] = task
			}
		}

		playerSettings.clear()

		nbt.map("settings") { it: CompoundTag ->
			for (k in it.allKeys) {
				playerSettings.computeIfAbsent(UUID.fromString(k), Object2ObjectFunction { PlayerSettings() })
					.deserializeNBT(it.getCompound(k))
			}
		}
	}

	override fun getTask(id: UUID): ReplicationTask? {
		return _tasks[id]?.asImmutable()
	}

	fun removeTask(id: UUID) {
		val task = _tasks[id] ?: return
		_tasks.remove(id)

		matterNode.graph.onMatterTaskRemoved(task)

		listeners.forEach { it.taskRemoved(task) }
		setChanged()
	}

	fun addTask(state: IPatternState, count: Int): IReplicationTask<*> {
		val task = ReplicationTask(UUID.randomUUID(), state.id, state.item, 0, 0, count)
		_tasks[task.id] = task

		matterNode.graph.onMatterTaskCreated(task)

		listeners.forEach { it.taskUpdated(task) }
		setChanged()

		return task
	}

	override fun dropAllTasks() {
		val graph = matterNode.graph as MatterGraph?

		for (task in _tasks.values) {
			graph?.onMatterTaskRemoved(task)
			listeners.forEach { menu: MatterPanelMenu -> menu.taskUpdated(task) }
		}

		_tasks.clear()
	}
}
