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

import com.google.common.collect.ImmutableList
import com.mojang.datafixers.util.Either
import it.unimi.dsi.fastutil.ints.Int2IntArrayMap
import it.unimi.dsi.fastutil.objects.Object2IntArrayMap
import it.unimi.dsi.fastutil.objects.Object2IntMap
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap
import net.minecraft.core.BlockPos
import net.minecraft.nbt.CompoundTag
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.item.DyeColor
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.material.Fluids
import net.minecraftforge.common.capabilities.ForgeCapabilities
import net.minecraftforge.fluids.FluidStack
import net.minecraftforge.fluids.capability.IFluidHandler
import ru.dbotthepony.mc.otm.block.entity.MatteryDeviceBlockEntity
import ru.dbotthepony.mc.otm.container.HandlerFilter
import ru.dbotthepony.mc.otm.container.MatteryContainer
import ru.dbotthepony.mc.otm.core.ifPresentK
import ru.dbotthepony.mc.otm.core.immutableList
import ru.dbotthepony.mc.otm.core.immutableMap
import ru.dbotthepony.mc.otm.core.isNotEmpty
import ru.dbotthepony.mc.otm.core.nbt.mapPresent
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.menu.decorative.PainterMenu
import ru.dbotthepony.mc.otm.registry.MBlockEntities
import java.util.*

class PainterBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryDeviceBlockEntity(MBlockEntities.PAINTER, blockPos, blockState), IFluidHandler {
	override fun createMenu(containerID: Int, inventory: Inventory, ply: Player): AbstractContainerMenu {
		return PainterMenu(containerID, inventory, this)
	}

	val dyeInput = MatteryContainer(this::markDirtyFast, 1)
	// null - water
	val dyeStored = Object2IntArrayMap<DyeColor?>()

	var isBulk = false
		set(value) {
			field = value
			markDirtyFast()
		}

	override fun getTanks(): Int {
		return 1
	}

	override fun getFluidInTank(tank: Int): FluidStack {
		if (tank != 1) return FluidStack.EMPTY
		if (waterStored() == 0) return FluidStack.EMPTY
		return FluidStack(Fluids.WATER, waterStored())
	}

	override fun getTankCapacity(tank: Int): Int {
		if (tank == 1) return MAX_WATER_STORAGE
		return 0
	}

	override fun isFluidValid(tank: Int, stack: FluidStack): Boolean {
		return tank == 0 && stack.fluid == Fluids.WATER
	}

	override fun fill(resource: FluidStack, action: IFluidHandler.FluidAction): Int {
		if (resource.isEmpty || resource.fluid != Fluids.WATER || waterStored() >= MAX_WATER_STORAGE) return 0
		val diff = (waterStored() + resource.amount).coerceAtMost(MAX_WATER_STORAGE) - waterStored()
		if (action.simulate() || diff == 0) return diff
		dyeStored[null] = waterStored() + diff
		return diff
	}

	override fun drain(resource: FluidStack, action: IFluidHandler.FluidAction): FluidStack {
		return FluidStack.EMPTY
	}

	override fun drain(maxDrain: Int, action: IFluidHandler.FluidAction): FluidStack {
		return FluidStack.EMPTY
	}

	init {
		addDroppableContainer(dyeInput)
		savetables.stateful(dyeInput, INVENTORY_KEY)
		savetables.bool(::isBulk)
		exposeGlobally(ForgeCapabilities.FLUID_HANDLER, this)
	}

	fun takeDyes(dyes: Map<out DyeColor?, Int>) {
		for ((dye, amount) in dyes) {
			if (dye == null) {
				for (i in 0 until amount) {
					if (waterStored() >= 1) {
						dyeStored[dye] = waterStored() - 1
					}
				}
			} else {
				for (i in 0 until amount) {
					mixer(dye)?.mix(dyeStored)

					if (dyeStored(dye) > 0) {
						dyeStored[dye] = dyeStored(dye) - 1
					}
				}
			}
		}

		markDirtyFast()
	}

	val config = ConfigurableItemHandler(input = dyeInput.handler(object : HandlerFilter {
		override fun canInsert(slot: Int, stack: ItemStack): Boolean {
			if (waterStored() < MAX_WATER_STORAGE) {
				stack.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).ifPresentK {
					val drain = it.drain(FluidStack(Fluids.WATER, MAX_WATER_STORAGE - waterStored()), IFluidHandler.FluidAction.SIMULATE)

					if (drain.isNotEmpty) {
						return true
					}
				}
			}

			val dye = DyeColor.entries.firstOrNull { stack.`is`(it.tag) } ?: return false
			return dyeStored(dye) + HUE_PER_ITEM <= MAX_STORAGE
		}

		override fun modifyInsertCount(slot: Int, stack: ItemStack, existing: ItemStack, simulate: Boolean): Int {
			if (!stack.equals(existing, false))
				return super.modifyInsertCount(slot, stack, existing, simulate)

			val dye = DyeColor.entries.firstOrNull { stack.`is`(it.tag) } ?: return 0
			return stack.count.coerceAtMost((MAX_STORAGE - dyeStored(dye)) / HUE_PER_ITEM - existing.count)
		}

		override fun canExtract(slot: Int, amount: Int, stack: ItemStack): Boolean {
			return false
		}
	}))

	fun waterStored(): Int {
		return dyeStored.getInt(null)
	}

	fun dyeStored(dye: DyeColor?): Int {
		return dyeStored.getInt(dye)
	}

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

		nbt["dyes"] = CompoundTag().also {
			for ((k, v) in dyeStored) {
				it[k?.getName() ?: "water"] = v
			}
		}
	}

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

		dyeStored.clear()

		nbt.mapPresent("dyes") { it: CompoundTag ->
			for (k in it.allKeys) {
				if (k == "water")
					dyeStored[null] = it.getInt("water")
				else
					dyeStored[DyeColor.entries.firstOrNull { it.getName() == k } ?: continue] = it.getInt(k)
			}
		}
	}

	override fun tick() {
		super.tick()

		for (slot in dyeInput.slotIterator()) {
			if (waterStored() < MAX_WATER_STORAGE) {
				slot.item.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM).ifPresentK {
					val drain = it.drain(FluidStack(Fluids.WATER, MAX_WATER_STORAGE - waterStored()), IFluidHandler.FluidAction.EXECUTE)

					if (drain.isNotEmpty) {
						dyeStored[null] = waterStored() + drain.amount
						slot.item = it.container
					}
				}
			}

			val dye = DyeColor.entries.firstOrNull { slot.item.`is`(it.tag) } ?: continue
			val stored = dyeStored(dye)

			if (stored + HUE_PER_ITEM <= MAX_STORAGE) {
				slot.item.shrink(1)
				slot.setChanged()
				dyeStored[dye] = stored + HUE_PER_ITEM
			}
		}
	}

	class Mixer(val color: DyeColor, vararg mixing: ImmutableList<Optional<DyeColor>>) : Map.Entry<DyeColor, Mixer> {
		val mixing: ImmutableList<ImmutableList<Optional<DyeColor>>> = ImmutableList.copyOf(mixing)

		override val key: DyeColor
			get() = color
		override val value: Mixer
			get() = this

		private fun mix(input: Object2IntMap<DyeColor?>, seen: MutableSet<DyeColor>, stack: MutableSet<DyeColor>) {
			if (input.getInt(color) > 0 || color in seen || color in stack) return
			stack.add(color)

			for (ingredients in mixing) {
				val copy = Object2IntArrayMap(input)
				var i = 0
				val volumes = Int2IntArrayMap()

				// страховка от взаимной блокировки когда последующий ингредиент требует предыдущий.
				// создаёт горку из значений
				for (i2 in ingredients.indices)
					volumes[i2] = 1

				while (!ingredients.all { copy.getInt(it.orElse(null)) > 0 } && i++ < 20) {
					ingredients.withIndex().forEach {
						for (t in 0 until volumes[it.index])
							it.value.ifPresent { mixer(it)?.mix(copy, seen, stack) }
					}

					var increase = i
					val iter = ingredients.indices.iterator()

					while (iter.hasNext() && increase-- > 0) {
						volumes[iter.nextInt()]++
					}
				}

				if (ingredients.all { copy.getInt(it.orElse(null)) > 0 }) {
					ingredients.forEach { copy[it.orElse(null)] = copy.getInt(it.orElse(null)) - 1 }
					copy[color] = ingredients.size

					input.putAll(copy)
					stack.remove(color)
					return
				}
			}

			stack.remove(color)
			seen.add(color)
		}

		fun mix(input: Object2IntMap<DyeColor?>) {
			mix(input, EnumSet.noneOf(DyeColor::class.java), EnumSet.noneOf(DyeColor::class.java))
		}

		fun isAvailable(input: Object2IntMap<DyeColor?>): Boolean {
			if (input.getInt(color) > 0) return true
			return Object2IntArrayMap(input).also(::mix).getInt(color) > 0
		}
	}

	companion object {
		fun mixer(color: DyeColor): Mixer? {
			return MIXING[color]
		}

		val MIXING = immutableMap<DyeColor, Mixer> {
			put(Mixer(DyeColor.BLACK, immutableList(Optional.of(DyeColor.CYAN), Optional.of(DyeColor.MAGENTA), Optional.of(DyeColor.YELLOW))))
			put(Mixer(DyeColor.RED, immutableList(Optional.of(DyeColor.MAGENTA), Optional.of(DyeColor.YELLOW))))
			put(Mixer(DyeColor.GREEN, immutableList(Optional.of(DyeColor.CYAN), Optional.of(DyeColor.YELLOW))))
			put(Mixer(DyeColor.BLUE, immutableList(Optional.of(DyeColor.CYAN), Optional.of(DyeColor.MAGENTA))))
			put(Mixer(DyeColor.LIGHT_BLUE, immutableList(Optional.of(DyeColor.WHITE), Optional.of(DyeColor.BLUE))))
			put(Mixer(DyeColor.LIME, immutableList(Optional.of(DyeColor.WHITE), Optional.of(DyeColor.GREEN))))
			put(Mixer(DyeColor.ORANGE, immutableList(Optional.of(DyeColor.WHITE), Optional.of(DyeColor.RED), Optional.empty())))
			put(Mixer(DyeColor.PURPLE, immutableList(Optional.of(DyeColor.CYAN), Optional.of(DyeColor.MAGENTA), Optional.empty())))
			put(Mixer(DyeColor.BROWN, immutableList(Optional.of(DyeColor.YELLOW), Optional.of(DyeColor.MAGENTA), Optional.of(DyeColor.BLACK), Optional.empty())))
			put(Mixer(DyeColor.PINK, immutableList(Optional.of(DyeColor.YELLOW), Optional.of(DyeColor.MAGENTA), Optional.of(DyeColor.WHITE), Optional.empty())))
			put(Mixer(DyeColor.GRAY, immutableList(Optional.of(DyeColor.WHITE), Optional.of(DyeColor.BLACK), Optional.empty())))
			put(Mixer(DyeColor.LIGHT_GRAY, immutableList(Optional.of(DyeColor.WHITE), Optional.of(DyeColor.WHITE), Optional.of(DyeColor.BLACK), Optional.empty())))
		}

		const val MAX_STORAGE = 256
		const val HUE_PER_ITEM = 32
		const val MAX_WATER_STORAGE = 4_000
	}
}
