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 net.minecraft.nbt.CompoundTag
import net.minecraft.nbt.ListTag
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.collect.WeakHashSet
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.matter.SimpleMatterNode
import ru.dbotthepony.mc.otm.menu.IItemSortingSettings
import ru.dbotthepony.mc.otm.registry.MBlockEntities
import java.util.*
import java.util.stream.Stream

class MatterPanelBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryDeviceBlockEntity(MBlockEntities.MATTER_PANEL, blockPos, blockState), IReplicationTaskProvider {
	inner class PlayerSettings : IItemSortingSettings {
		override var sorting: ItemSorter = ItemSorter.DEFAULT
			set(value) {
				field = value
				markDirtyFast()
			}

		override var isAscending: Boolean = true
			set(value) {
				field = value
				markDirtyFast()
			}
	}

	var isProvidingTasks = true
		set(value) {
			if (field != value) {
				field = value

				if (value) {
					_tasks.values.forEach { matterNode.graph.onMatterTaskCreated(it) }
				} else {
					_tasks.values.forEach { if (it.inProgress == 0) matterNode.graph.onMatterTaskRemoved(it) }
				}

				markDirtyFast()
			}
		}

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

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

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

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

	fun detachMenu(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)

		savetables.bool(::isProvidingTasks)
	}

	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? {
		if (!isProvidingTasks) return null

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

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

				return ReplicationTaskAllocation(task, pattern)
			}
		}

		return null
	}

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

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

		markDirtyFast()
		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]
	}

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

		matterNode.graph.onMatterTaskRemoved(task)

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

	fun addTask(state: PatternState, count: Int): ReplicationTask {
		val task = ReplicationTask(UUID.randomUUID(), Optional.of(state.id), state.item, 0, 0, count)
		_tasks[task.id] = task

		if (isProvidingTasks)
			matterNode.graph.onMatterTaskCreated(task)

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

		return task
	}

	override fun dropAllTasks() {
		for (task in _tasks.values) {
			matterNode.graph.onMatterTaskRemoved(task)
			listeners.forEach { it.taskRemoved(task) }
		}

		_tasks.clear()
	}
}
