package ru.dbotthepony.mc.otm.menu

import com.google.common.collect.ImmutableList
import net.minecraft.world.Container
import net.minecraft.world.SimpleContainer
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.Slot
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.neoforged.neoforge.capabilities.Capabilities
import ru.dbotthepony.kommons.util.Delegate
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.value
import ru.dbotthepony.mc.otm.capability.FlowDirection
import ru.dbotthepony.mc.otm.capability.IMatteryUpgrade
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.UpgradeType
import ru.dbotthepony.mc.otm.capability.energy
import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.container.IMatteryContainer
import ru.dbotthepony.mc.otm.container.ItemFilter
import ru.dbotthepony.mc.otm.container.UpgradeContainer
import ru.dbotthepony.mc.otm.core.collect.ConditionalEnumSet
import ru.dbotthepony.mc.otm.core.immutableList
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.menu.input.BooleanInputWithFeedback
import ru.dbotthepony.mc.otm.menu.input.InstantBooleanInput
import ru.dbotthepony.mc.otm.runOnClient
import java.util.*
import java.util.function.BooleanSupplier
import java.util.function.DoubleSupplier
import java.util.function.IntSupplier
import java.util.function.Predicate
import java.util.function.Supplier
import kotlin.reflect.KMutableProperty0

/**
 * Make slots for single container
 */
inline fun <C : Container, S : Slot> makeSlots(container: C, initializer: (C, Int) -> S): ImmutableList<S> {
	return immutableList(container.containerSize) { initializer.invoke(container, it) }
}

/**
 * Make slots for list of containers with single slot in them
 */
inline fun <S : Slot> makeSlots(containers: List<Container>?, size: Int, initializer: (Container, Int) -> S): ImmutableList<S> {
	return immutableList(size) { initializer(containers?.get(it) ?: SimpleContainer(1), 0) }
}

open class MatterySlot(container: Container, index: Int, x: Int = 0, y: Int = 0) : Slot(container, index, x, y) {
	var ignoreSpectators = true

	override fun setChanged() {
		if (container is IMatteryContainer) {
			(container as IMatteryContainer).setChanged(containerSlot)
		} else {
			super.setChanged()
		}
	}

	override fun mayPickup(player: Player): Boolean {
		return super.mayPickup(player) && (!ignoreSpectators || !player.isSpectator)
	}

	open fun canTakeItemForPickAll(): Boolean {
		return true
	}

	override fun getMaxStackSize(): Int {
		val container = container

		if (container is IMatteryContainer) {
			return container.getMaxStackSize(slotIndex, ItemStack.EMPTY)
		} else {
			return super.getMaxStackSize()
		}
	}

	override fun getMaxStackSize(itemStack: ItemStack): Int {
		val container = container

		if (container is IMatteryContainer) {
			return container.getMaxStackSize(slotIndex, itemStack)
		} else {
			return super.getMaxStackSize(itemStack)
		}
	}
}

open class UserFilteredSlot(container: Container, index: Int, x: Int = 0, y: Int = 0) : MatterySlot(container, index, x, y), Predicate<ItemStack> {
	var hasSetFilter = false
		private set

	var filter: Delegate<Item?>? = null
		set(value) {
			hasSetFilter = true
			field = value
		}

	override fun canTakeItemForPickAll(): Boolean {
		return filter?.get() == null
	}

	override fun test(t: ItemStack): Boolean {
		return filter?.get() == null || filter?.get() == t.item
	}

	fun isSameFilter(other: Slot): Boolean {
		if (other !is UserFilteredSlot)
			return filter?.get() == null

		return (
			(other.filter == null && filter == null) ||
			(other.filter != null && filter != null && other.filter!!.get() == filter!!.get())
		)
	}

	override fun isSameInventory(other: Slot): Boolean {
		return isSameFilter(other) && super.isSameInventory(other)
	}
}

open class OutputSlot(container: Container, index: Int, x: Int = 0, y: Int = 0, val onTake: (ItemStack) -> Unit = {}) : MatterySlot(container, index, x, y) {
	override fun mayPlace(itemStack: ItemStack): Boolean {
		return false
	}

	override fun onTake(pPlayer: Player, pStack: ItemStack) {
		super.onTake(pPlayer, pStack)
		this.onTake.invoke(pStack)
	}

	override fun onQuickCraft(pStack: ItemStack, pAmount: Int) {
		super.onQuickCraft(pStack, pAmount)
		this.onTake.invoke(pStack)
	}
}

open class BatterySlot(container: Container, index: Int, x: Int = 0, y: Int = 0) : MatterySlot(container, index, x, y) {
	override fun mayPlace(itemStack: ItemStack): Boolean {
		return super.mayPlace(itemStack) && (itemStack.energy?.canExtract() ?: false)
	}
}

open class ChemicalFuelSlot(container: Container, index: Int, x: Int = 0, y: Int = 0) : MatterySlot(container, index, x, y) {
	override fun mayPlace(itemStack: ItemStack): Boolean {
		return super.mayPlace(itemStack) && itemStack.getBurnTime(null) > 0
	}
}

open class ChargeSlot(container: Container, index: Int, x: Int = 0, y: Int = 0) : MatterySlot(container, index, x, y) {
	override fun mayPlace(itemStack: ItemStack): Boolean {
		return super.mayPlace(itemStack) && (itemStack.energy?.canReceive() ?: false)
	}
}

open class EnergyContainerInputSlot(container: Container, index: Int, x: Int = 0, y: Int = 0, val direction: FlowDirection = FlowDirection.BI_DIRECTIONAL) : MatterySlot(container, index, x, y) {
	override fun mayPlace(itemStack: ItemStack): Boolean {
		return super.mayPlace(itemStack) && (itemStack.getCapability(Capabilities.EnergyStorage.ITEM)?.let { direction.test(FlowDirection.of(it.canReceive(), it.canExtract())) } ?: false)
	}
}

open class MatterContainerInputSlot(
	container: Container,
	index: Int,
	x: Int = 0,
	y: Int = 0,
	val direction: FlowDirection = FlowDirection.BI_DIRECTIONAL
) : MatterySlot(container, index, x, y) {
	override fun mayPlace(itemStack: ItemStack): Boolean {
		val handler = itemStack.getCapability(MatteryCapability.MATTER_ITEM)
		return handler != null && super.mayPlace(itemStack) && this.direction.test(handler.matterFlow)
	}
}

open class PatternSlot(container: Container, index: Int, x: Int = 0, y: Int = 0) : MatterySlot(container, index, x, y) {
	override fun mayPlace(itemStack: ItemStack): Boolean {
		return super.mayPlace(itemStack) && itemStack.getCapability(MatteryCapability.PATTERN_ITEM) != null
	}
}

open class DriveSlot(container: Container, index: Int, x: Int = 0, y: Int = 0) : MatterySlot(container, index, x, y) {
	override fun mayPlace(itemStack: ItemStack): Boolean {
		return super.mayPlace(itemStack) && itemStack.getCapability(MatteryCapability.CONDENSATION_DRIVE) != null
	}
}

fun MatteryMenu.addFilterSlots(slots: Delegate<ItemFilter>): List<Delegate<ItemStack>> {
	val result = ArrayList<Delegate<ItemStack>>(slots.value.size)

	for (i in 0 until slots.value.size) {
		result.add(Delegate.Of(
			mSynchronizer.computedItem { slots.value[i] },
			itemStackInput { slots.value = slots.value.set(i, it) }
		))
	}

	return result
}

fun MatteryMenu.addFilterSlots(amount: Int): List<Delegate<ItemStack>> {
	val result = ArrayList<Delegate<ItemStack>>(amount)

	for (i in 0 until amount) {
		result.add(Delegate.Of(
			mSynchronizer.computedItem { ItemStack.EMPTY },
			itemStackInput { throw UnsupportedOperationException() }
		))
	}

	return result
}

fun MatteryMenu.addFilterSlots(slots: Delegate<ItemFilter>?, amount: Int): List<Delegate<ItemStack>> {
	if (slots != null && amount != slots.value.size)
		throw IllegalStateException("Provided ItemFiler has different amount of slots than expected: ${slots.value.size} != $amount")

	if (slots == null)
		return addFilterSlots(amount)
	else
		return addFilterSlots(slots)
}

fun MatteryMenu.addFilterSlots(slots: KMutableProperty0<ItemFilter>?, amount: Int): List<Delegate<ItemStack>> {
	return addFilterSlots(if (slots == null) null else Delegate.Of(slots), amount)
}

data class FilterControls(val slots: List<Delegate<ItemStack>>, val isWhitelist: BooleanInputWithFeedback, val matchComponents: BooleanInputWithFeedback, val matchTag: BooleanInputWithFeedback)

fun MatteryMenu.addFilterControls(slots: Delegate<ItemFilter>?, amount: Int): FilterControls {
	if (slots == null) {
		return FilterControls(addFilterSlots(amount), BooleanInputWithFeedback(this), BooleanInputWithFeedback(this), BooleanInputWithFeedback(this))
	} else {
		return FilterControls(
			addFilterSlots(slots, amount),
			BooleanInputWithFeedback.dispatch(this, slots, ItemFilter::isWhitelist, ItemFilter::isWhitelist),
			BooleanInputWithFeedback.dispatch(this, slots, ItemFilter::matchComponents, ItemFilter::matchComponents),
			BooleanInputWithFeedback.dispatch(this, slots, ItemFilter::matchTag, ItemFilter::matchTag),
		)
	}
}

fun MatteryMenu.addFilterControls(slots: KMutableProperty0<ItemFilter>?, amount: Int): FilterControls {
	return addFilterControls(slots?.let { Delegate.Of(it) }, amount)
}

/**
 * [openState] **is clientside only**, attempting to use it on server will result
 * in classloading exceptions.
 */
data class UpgradeSlots(
	val slots: List<MatterySlot>,
	val allowedTypes: Set<UpgradeType>,
	val openState: Delegate<Boolean>,
	val currentStats: IMatteryUpgrade,
	val areLocked: Supplier<Boolean>
)

fun MatteryMenu.makeUpgradeSlots(count: Int, container: UpgradeContainer?): UpgradeSlots {
	if (container != null) {
		require(count == container.containerSize) { "Upgrade container size ${container.containerSize} does not match with provided size $count" }
	}

	val shouldLockUpgradeSlots: Supplier<Boolean>
	val allowedTypes = EnumMap<UpgradeType, BooleanSupplier>(UpgradeType::class.java)

	if (container == null) {
		shouldLockUpgradeSlots = mSynchronizer.boolean()
	} else {
		shouldLockUpgradeSlots = mSynchronizer.computedBoolean(container.shouldLockUpgradeSlots)
	}

	for (value in UpgradeType.ALL) {
		val b = mSynchronizer.boolean().delegate.also {
			if (container != null) {
				it.accept(value in container.allowedUpgrades)
			}
		}

		allowedTypes[value] = BooleanSupplier { b.get() }
	}

	val syncContainer = container ?: SimpleContainer(count)
	val isOpen = InstantBooleanInput(this)

	return UpgradeSlots(
		slots = immutableList(count) {
			object : MatterySlot(syncContainer, it) {
				init {
					mapQuickMoveToInventory(this)
				}

				override fun mayPlace(itemStack: ItemStack): Boolean {
					return super.mayPlace(itemStack) &&
						(itemStack.getCapability(MatteryCapability.UPGRADE)?.let { it.upgradeTypes.any { allowedTypes[it]!!.asBoolean } } == true) &&
						!shouldLockUpgradeSlots.get()
				}

				override fun mayPickup(player: Player): Boolean {
					return super.mayPickup(player) && !shouldLockUpgradeSlots.get()
				}
			}
		}.also { for (i in it.indices.reversed()) addStorageSlot(it[i], prepend = true, condition = isOpen) },

		areLocked = shouldLockUpgradeSlots,

		allowedTypes = ConditionalEnumSet(allowedTypes),
		openState = isOpen,
		currentStats = object : IMatteryUpgrade {
			override val upgradeTypes: Set<UpgradeType> = setOf()
			override val speedBonus: Double by mSynchronizer.computedDouble(DoubleSupplier { container?.speedBonus ?: 0.0 })
			override val processingItems: Int by mSynchronizer.computedInt(IntSupplier { container?.processingItems ?: 0 })
			override val energyStorageFlat: Decimal by mSynchronizer.computedDecimal { container?.energyStorageFlat ?: Decimal.ZERO }
			override val energyStorage: Decimal by mSynchronizer.computedDecimal { container?.energyStorage ?: Decimal.ZERO }
			override val energyConsumed: Decimal by mSynchronizer.computedDecimal { container?.energyConsumed ?: Decimal.ZERO }
			override val energyThroughputFlat: Decimal by mSynchronizer.computedDecimal { container?.energyThroughputFlat ?: Decimal.ZERO }
			override val energyThroughput: Decimal by mSynchronizer.computedDecimal { container?.energyThroughput ?: Decimal.ZERO }
			override val failureMultiplier: Double by mSynchronizer.computedDouble(DoubleSupplier { container?.failureMultiplier ?: 1.0 })
		}
	)
}
