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

import com.google.common.collect.ImmutableSet
import mekanism.common.tile.interfaces.IRedstoneControl
import net.minecraft.world.level.block.entity.BlockEntityType
import net.minecraft.core.BlockPos
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.MenuProvider
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.chat.Component
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.Level
import net.minecraftforge.common.capabilities.ForgeCapabilities
import net.minecraftforge.fluids.FluidStack
import net.minecraftforge.fluids.capability.IFluidHandler
import net.minecraftforge.items.IItemHandler
import ru.dbotthepony.mc.otm.capability.CombinedItemHandler
import ru.dbotthepony.mc.otm.capability.EmptyItemHandler
import ru.dbotthepony.mc.otm.capability.FlowDirection
import ru.dbotthepony.mc.otm.capability.UnmodifiableItemHandler
import ru.dbotthepony.mc.otm.capability.energy.IMatteryEnergyStorage
import ru.dbotthepony.mc.otm.capability.moveBetweenSlots
import ru.dbotthepony.mc.otm.capability.moveEnergy
import ru.dbotthepony.mc.otm.capability.moveFluid
import ru.dbotthepony.mc.otm.core.TextComponent
import ru.dbotthepony.mc.otm.core.getValue
import ru.dbotthepony.mc.otm.core.ifPresentK
import ru.dbotthepony.mc.otm.core.immutableMap
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.RelativeSide
import ru.dbotthepony.mc.otm.core.nbt.getJson
import ru.dbotthepony.mc.otm.core.nbt.putJson
import ru.dbotthepony.mc.otm.core.util.ITickable
import ru.dbotthepony.mc.otm.once

/**
 * Device block entity base, implementing [MenuProvider] and [IRedstoneControlled], and also tracks custom display name
 */
abstract class MatteryDeviceBlockEntity(blockEntityType: BlockEntityType<*>, blockPos: BlockPos, blockState: BlockState)
	: MatteryBlockEntity(blockEntityType, blockPos, blockState), MenuProvider, IRedstoneControlled {
	var customDisplayName: Component? = null

	override val redstoneControl: AbstractRedstoneControl = RedstoneControl { new, old ->
		setChangedLight()

		if (new != old)
			redstoneStatusUpdated(new, old)
	}

	init {
		savetables.stateful(::redstoneControl, REDSTONE_CONTROL_KEY)
	}

	protected open val defaultDisplayName: Component
		get() = level?.getBlockState(blockPos)?.block?.name ?: TextComponent("null at $blockPos")

	abstract override fun createMenu(containerID: Int, inventory: Inventory, ply: Player): AbstractContainerMenu?

	override fun getDisplayName(): Component {
		return customDisplayName ?: defaultDisplayName
	}

	protected open fun redstoneStatusUpdated(newBlocked: Boolean, oldBlocked: Boolean) {}

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

		if (customDisplayName != null)
			nbt.putJson("Name", Component.Serializer.toJsonTree(customDisplayName!!))
	}

	override fun load(nbt: CompoundTag) {
		super.load(nbt)
		customDisplayName = nbt.getJson("Name")?.let(Component.Serializer::fromJson)
	}

	override fun setLevel(level: Level) {
		super.setLevel(level)

		level.once {
			if (!isRemoved && this.level == level) {
				redstoneControl.redstoneSignal = level.getBestNeighborSignal(blockPos)
			}
		}
	}

	inner class ConfigurableFluidHandler<T : IFluidHandler>(
		val capability: T,

		val possibleModes: FlowDirection = FlowDirection.BI_DIRECTIONAL,

		val frontDefault: FlowDirection = possibleModes,
		val backDefault: FlowDirection = possibleModes,
		val leftDefault: FlowDirection = possibleModes,
		val rightDefault: FlowDirection = possibleModes,
		val topDefault: FlowDirection = possibleModes,
		val bottomDefault: FlowDirection = possibleModes,
	) {
		init {
			exposeSideless(ForgeCapabilities.FLUID_HANDLER, capability)
		}

		val front = Piece(RelativeSide.FRONT).also { it.flow = frontDefault }
		val back = Piece(RelativeSide.BACK).also { it.flow = backDefault }
		val left = Piece(RelativeSide.LEFT).also { it.flow = leftDefault }
		val right = Piece(RelativeSide.RIGHT).also { it.flow = rightDefault }
		val top = Piece(RelativeSide.TOP).also { it.flow = topDefault }
		val bottom = Piece(RelativeSide.BOTTOM).also { it.flow = bottomDefault }

		val pieces = immutableMap {
			put(RelativeSide.FRONT, front)
			put(RelativeSide.BACK, back)
			put(RelativeSide.LEFT, left)
			put(RelativeSide.RIGHT, right)
			put(RelativeSide.TOP, top)
			put(RelativeSide.BOTTOM, bottom)
		}

		val defaults = immutableMap {
			put(RelativeSide.FRONT, frontDefault)
			put(RelativeSide.BACK, backDefault)
			put(RelativeSide.LEFT, leftDefault)
			put(RelativeSide.RIGHT, rightDefault)
			put(RelativeSide.TOP, topDefault)
			put(RelativeSide.BOTTOM, bottomDefault)
		}

		inner class Piece(val side: RelativeSide) : IFluidHandler, ITickable {
			init {
				tickList.always(this)

				// https://tenor.com/view/simp-metal-gear-liquid-snake-running-gif-16717852
				savetables.enum(::flow, "fluid_${side}_flow", FlowDirection::valueOf)
				savetables.bool(::automatePull, "fluid_${side}_pull")
				savetables.bool(::automatePush, "fluid_${side}_push")
			}

			private val controller = sides[side]!!.Cap(ForgeCapabilities.FLUID_HANDLER, this)
			private val neighbour by sides[side]!!.track(ForgeCapabilities.FLUID_HANDLER)

			var flow by synchronizer.enum(possibleModes, setter = { value, access, setByRemote ->
				require(possibleModes.isSupertype(value)) { "Energy mode $value is not allowed (allowed modes: ${possibleModes.family})" }

				if (access.read() != value) {
					access.write(value)
					setChangedLight()

					if (value == FlowDirection.NONE) {
						controller.close()
					} else {
						controller.close()
						controller.expose()
					}
				}
			})

			// var automatePull by synchronizer.bool().property
			var automatePull = false
				set(value) {
					field = value
					setChangedLight()
				}
			// var automatePush by synchronizer.bool().property
			var automatePush = false
				set(value) {
					field = value
					setChangedLight()
				}

			override fun tick() {
				if (flow == FlowDirection.NONE || !automatePull && !automatePush || redstoneControl.isBlockedByRedstone)
					return

				neighbour.ifPresentK {
					if (flow.input && automatePull) {
						moveFluid(source = it, destination = capability)
					}

					if (flow.output && automatePush) {
						moveFluid(source = capability, destination = it)
					}
				}
			}

			override fun getTanks(): Int {
				if (flow == FlowDirection.NONE) {
					return 0
				} else {
					return capability.getTanks()
				}
			}

			override fun getFluidInTank(tank: Int): FluidStack {
				if (flow == FlowDirection.NONE) {
					return FluidStack.EMPTY
				} else {
					return capability.getFluidInTank(tank)
				}
			}

			override fun getTankCapacity(tank: Int): Int {
				if (flow == FlowDirection.NONE) {
					return 0
				} else {
					return capability.getTankCapacity(tank)
				}
			}

			override fun isFluidValid(tank: Int, stack: FluidStack): Boolean {
				if (flow.input) {
					return capability.isFluidValid(tank, stack)
				} else {
					return false
				}
			}

			override fun fill(resource: FluidStack, action: IFluidHandler.FluidAction): Int {
				if (flow.input) {
					return capability.fill(resource, action)
				} else {
					return 0
				}
			}

			override fun drain(resource: FluidStack, action: IFluidHandler.FluidAction): FluidStack {
				if (flow.output) {
					return capability.drain(resource, action)
				} else {
					return FluidStack.EMPTY
				}
			}

			override fun drain(maxDrain: Int, action: IFluidHandler.FluidAction): FluidStack {
				if (flow.output) {
					return capability.drain(maxDrain, action)
				} else {
					return FluidStack.EMPTY
				}
			}
		}
	}

	inner class ConfigurableEnergy<T : IMatteryEnergyStorage>(
		val energy: T,

		val modesFront: FlowDirection = energy.energyFlow,
		val modesBack: FlowDirection = energy.energyFlow,
		val modesLeft: FlowDirection = energy.energyFlow,
		val modesRight: FlowDirection = energy.energyFlow,
		val modesTop: FlowDirection = energy.energyFlow,
		val modesBottom: FlowDirection = energy.energyFlow,

		val frontDefault: FlowDirection = modesFront,
		val backDefault: FlowDirection = modesBack,
		val leftDefault: FlowDirection = modesLeft,
		val rightDefault: FlowDirection = modesRight,
		val topDefault: FlowDirection = modesTop,
		val bottomDefault: FlowDirection = modesBottom,
	) {
		constructor(
			energy: T,
			possibleModes: FlowDirection,
			frontDefault: FlowDirection = possibleModes,
			backDefault: FlowDirection = possibleModes,
			leftDefault: FlowDirection = possibleModes,
			rightDefault: FlowDirection = possibleModes,
			topDefault: FlowDirection = possibleModes,
			bottomDefault: FlowDirection = possibleModes,
		) : this(
			energy,
			modesFront = possibleModes, frontDefault = frontDefault,
			modesBack = possibleModes, backDefault = backDefault,
			modesLeft = possibleModes, leftDefault = leftDefault,
			modesRight = possibleModes, rightDefault = rightDefault,
			modesTop = possibleModes, topDefault = topDefault,
			modesBottom = possibleModes, bottomDefault = bottomDefault,
		)

		init {
			exposeEnergySideless(energy)
		}

		val front = Piece(RelativeSide.FRONT, modesFront).also { it.energyFlow = frontDefault }
		val back = Piece(RelativeSide.BACK, modesBack).also { it.energyFlow = backDefault }
		val left = Piece(RelativeSide.LEFT, modesLeft).also { it.energyFlow = leftDefault }
		val right = Piece(RelativeSide.RIGHT, modesRight).also { it.energyFlow = rightDefault }
		val top = Piece(RelativeSide.TOP, modesTop).also { it.energyFlow = topDefault }
		val bottom = Piece(RelativeSide.BOTTOM, modesBottom).also { it.energyFlow = bottomDefault }

		val pieces = immutableMap {
			put(RelativeSide.FRONT, front)
			put(RelativeSide.BACK, back)
			put(RelativeSide.LEFT, left)
			put(RelativeSide.RIGHT, right)
			put(RelativeSide.TOP, top)
			put(RelativeSide.BOTTOM, bottom)
		}

		val defaults = immutableMap {
			put(RelativeSide.FRONT, frontDefault)
			put(RelativeSide.BACK, backDefault)
			put(RelativeSide.LEFT, leftDefault)
			put(RelativeSide.RIGHT, rightDefault)
			put(RelativeSide.TOP, topDefault)
			put(RelativeSide.BOTTOM, bottomDefault)
		}

		fun invalidate(force: Boolean = false) {
			for (piece in pieces.values) {
				piece.invalidate(force)
			}
		}

		inner class Piece(val side: RelativeSide, val possibleModes: FlowDirection) : IMatteryEnergyStorage, ITickable {
			private val capControllers = exposeEnergy(side, this@Piece)
			private val neighbour by sides[side]!!.trackEnergy()

			override var batteryLevel: Decimal by energy::batteryLevel
			override val maxBatteryLevel: Decimal by energy::maxBatteryLevel
			override val missingPower: Decimal by energy::missingPower

			override val canSetBatteryLevel: Boolean by energy::canSetBatteryLevel

			// var automatePull by synchronizer.bool().property
			var automatePull = false
				set(value) {
					field = value
					setChangedLight()
				}
			// var automatePush by synchronizer.bool().property
			var automatePush = false
				set(value) {
					field = value
					setChangedLight()
				}

			init {
				tickList.always(this)

				savetables.enum(::energyFlow, "energy_${side}_flow", FlowDirection::valueOf)
				savetables.bool(::automatePull, "energy_${side}_pull")
				savetables.bool(::automatePush, "energy_${side}_push")
			}

			override fun extractEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
				return energy.extractEnergy(howMuch, simulate)
			}

			override fun receiveEnergy(howMuch: Decimal, simulate: Boolean): Decimal {
				return energy.receiveEnergy(howMuch, simulate)
			}

			override fun extractEnergyChecked(howMuch: Decimal, simulate: Boolean): Decimal {
				if (energyFlow.output)
					return energy.extractEnergyChecked(howMuch, simulate)

				return Decimal.ZERO
			}

			override fun receiveEnergyChecked(howMuch: Decimal, simulate: Boolean): Decimal {
				if (energyFlow.input)
					return energy.receiveEnergyChecked(howMuch, simulate)

				return Decimal.ZERO
			}

			override fun drainBattery(): Boolean {
				return energy.drainBattery()
			}

			override fun fillBattery(): Boolean {
				return energy.fillBattery()
			}

			override fun tick() {
				if (energyFlow == FlowDirection.NONE || !automatePull && !automatePush || redstoneControl.isBlockedByRedstone)
					return

				neighbour.ifPresentK {
					if (energyFlow.input && automatePull) {
						moveEnergy(source = it, destination = energy, simulate = false)
					}

					if (energyFlow.output && automatePush) {
						moveEnergy(source = energy, destination = it, simulate = false)
					}
				}
			}

			override var energyFlow by synchronizer.enum(possibleModes, setter = { value, access, setByRemote ->
				require(possibleModes.isSupertype(value)) { "Energy mode $value is not allowed (allowed modes: ${possibleModes.family})" }

				if (access.read() != value) {
					access.write(value)
					setChangedLight()

					if (value == FlowDirection.NONE) {
						for (controller in capControllers)
							controller.close()
					} else {
						for (controller in capControllers) {
							controller.close()
							controller.expose()
						}
					}
				}
			})

			fun invalidate(force: Boolean = false) {
				if (force) {
					for (controller in capControllers) {
						controller.close()
						controller.expose()
					}

					if (energyFlow == FlowDirection.NONE) {
						for (controller in capControllers) {
							controller.close()
						}
					}
				} else {
					if (energyFlow != FlowDirection.NONE) {
						for (controller in capControllers) {
							controller.close()
							controller.expose()
						}
					}
				}
			}
		}
	}

	enum class ItemHandlerMode(val translationKey: String) {
		DISABLED("otm.gui.side_mode.disabled"),
		INPUT("otm.gui.side_mode.input"),
		OUTPUT("otm.gui.side_mode.output"),
		INPUT_OUTPUT("otm.gui.side_mode.input_output"),
		BATTERY("otm.gui.side_mode.battery");
	}

	inner class ConfigurableItemHandler(
		input: IItemHandler? = null,
		output: IItemHandler? = null,
		inputOutput: IItemHandler? = null,
		val battery: IItemHandler? = null,
		val frontDefault: ItemHandlerMode = determineDefaultMode(input, output, inputOutput, battery, RelativeSide.FRONT),
		val backDefault: ItemHandlerMode = determineDefaultMode(input, output, inputOutput, battery, RelativeSide.BACK),
		val leftDefault: ItemHandlerMode = determineDefaultMode(input, output, inputOutput, battery, RelativeSide.LEFT),
		val rightDefault: ItemHandlerMode = determineDefaultMode(input, output, inputOutput, battery, RelativeSide.RIGHT),
		val topDefault: ItemHandlerMode = determineDefaultMode(input, output, inputOutput, battery, RelativeSide.TOP),
		val bottomDefault: ItemHandlerMode = determineDefaultMode(input, output, inputOutput, battery, RelativeSide.BOTTOM),
	) {
		val sideless: IItemHandler
		val possibleViews: ImmutableSet<ItemHandlerMode>
		val inputOutput: IItemHandler?
		val input: IItemHandler?
		val output: IItemHandler?

		init {
			if ((input != null || output != null) && inputOutput != null)
				throw IllegalArgumentException("Either specify input or/and output separately, or specify inputOutput")

			if (inputOutput != null) {
				this.input = inputOutput
				this.output = inputOutput
			} else {
				this.input = input
				this.output = output
			}

			val builder = ImmutableSet.Builder<ItemHandlerMode>()

			builder.add(ItemHandlerMode.DISABLED)

			if (input != null) builder.add(ItemHandlerMode.INPUT)
			if (output != null) builder.add(ItemHandlerMode.OUTPUT)
			if (input != null && output != null) builder.add(ItemHandlerMode.INPUT_OUTPUT)

			if (inputOutput != null) {
				builder.add(ItemHandlerMode.INPUT)
				builder.add(ItemHandlerMode.OUTPUT)
				builder.add(ItemHandlerMode.INPUT_OUTPUT)
			}

			if (battery != null) builder.add(ItemHandlerMode.BATTERY)

			possibleViews = builder.build()

			if (input != null && output != null) {
				this.inputOutput = CombinedItemHandler(input, output)
			} else {
				this.inputOutput = inputOutput
			}

			val caps = ArrayList<IItemHandler>()

			if (input != null) caps.add(input)
			if (output != null) caps.add(output)
			if (inputOutput != null) caps.add(inputOutput)
			if (battery != null) caps.add(battery)

			sideless = UnmodifiableItemHandler(CombinedItemHandler(caps))
			exposeSideless(ForgeCapabilities.ITEM_HANDLER, sideless)
		}

		val front = Piece(RelativeSide.FRONT).also { it.mode = frontDefault }
		val back = Piece(RelativeSide.BACK).also { it.mode = backDefault }
		val left = Piece(RelativeSide.LEFT).also { it.mode = leftDefault }
		val right = Piece(RelativeSide.RIGHT).also { it.mode = rightDefault }
		val top = Piece(RelativeSide.TOP).also { it.mode = topDefault }
		val bottom = Piece(RelativeSide.BOTTOM).also { it.mode = bottomDefault }

		val pieces = immutableMap {
			put(RelativeSide.FRONT, front)
			put(RelativeSide.BACK, back)
			put(RelativeSide.LEFT, left)
			put(RelativeSide.RIGHT, right)
			put(RelativeSide.TOP, top)
			put(RelativeSide.BOTTOM, bottom)
		}

		val defaults = immutableMap {
			put(RelativeSide.FRONT, frontDefault)
			put(RelativeSide.BACK, backDefault)
			put(RelativeSide.LEFT, leftDefault)
			put(RelativeSide.RIGHT, rightDefault)
			put(RelativeSide.TOP, topDefault)
			put(RelativeSide.BOTTOM, bottomDefault)
		}

		inner class Piece(val side: RelativeSide) : IItemHandler, ITickable {
			private var currentHandler: IItemHandler = EmptyItemHandler
			private val capController = sides[side]!!.Cap(ForgeCapabilities.ITEM_HANDLER, this)
			private val neighbour by sides[side]!!.track(ForgeCapabilities.ITEM_HANDLER)

			init {
				tickList.always(this)
				capController.close()
			}

			var mode by synchronizer.enum(ItemHandlerMode.DISABLED, setter = { value, access, setByRemote ->
				require(value in possibleViews) { "View type $value is not allowed (allowed views: $possibleViews)" }

				if (access.read() != value) {
					access.write(value)
					setChangedLight()

					if (value == ItemHandlerMode.DISABLED) {
						capController.close()
					} else {
						capController.close()
						capController.expose()
					}

					when (value) {
						ItemHandlerMode.DISABLED -> currentHandler = EmptyItemHandler
						ItemHandlerMode.INPUT -> currentHandler = input!!
						ItemHandlerMode.OUTPUT -> currentHandler = output!!
						ItemHandlerMode.INPUT_OUTPUT -> currentHandler = inputOutput!!
						ItemHandlerMode.BATTERY -> currentHandler = battery!!
					}
				}
			})

			/*var automatePull by synchronizer.bool(setter = { value, access, _ ->
				if (access.readBoolean() != value) {
					access.write(value)

					if (value) {
						innerSlotPush = 0
						outerSlotPush = 0
					}
				}
			}).property

			var automatePush by synchronizer.bool(setter = { value, access, _ ->
				if (access.readBoolean() != value) {
					access.write(value)

					if (value) {
						innerSlotPush = 0
						outerSlotPush = 0
					}
				}
			}).property*/

			var automatePull = false
				set(value) {
					if (field != value) {
						field = value
						setChangedLight()

						if (value) {
							innerSlotPush = 0
							outerSlotPush = 0
						}
					}
				}

			var automatePush = false
				set(value) {
					if (field != value) {
						field = value
						setChangedLight()

						if (value) {
							innerSlotPush = 0
							outerSlotPush = 0
						}
					}
				}

			init {
				savetables.bool(::automatePull, "itemhandler_${side}_automatePull")
				savetables.bool(::automatePush, "itemhandler_${side}_automatePush")
				savetables.enum(::mode, "itemhandler_${side}_mode", ItemHandlerMode::valueOf)
			}

			private var innerSlotPull = 0
			private var outerSlotPull = 0

			private var innerSlotPush = 0
			private var outerSlotPush = 0

			override fun tick() {
				if (mode == ItemHandlerMode.DISABLED || !automatePull && !automatePush || redstoneControl.isBlockedByRedstone || currentHandler.slots == 0)
					return

				neighbour.ifPresentK {
					if (it.slots == 0)
						return

					if (automatePull) {
						if (innerSlotPull !in 0 until currentHandler.slots)
							innerSlotPull = 0

						if (outerSlotPull !in 0 until it.slots)
							outerSlotPull = 0

						val (outerSlotPull, innerSlotPull) = moveBetweenSlots(it, outerSlotPull, currentHandler, innerSlotPull)
						this.innerSlotPull = innerSlotPull
						this.outerSlotPull = outerSlotPull
					}

					if (automatePush) {
						if (innerSlotPush !in 0 until currentHandler.slots)
							innerSlotPush = 0

						if (outerSlotPush !in 0 until it.slots)
							outerSlotPush = 0

						val (innerSlotPush, outerSlotPush) = moveBetweenSlots(currentHandler, innerSlotPush, it, outerSlotPush)
						this.innerSlotPush = innerSlotPush
						this.outerSlotPush = outerSlotPush
					}
				}
			}

			override fun getSlots(): Int {
				return currentHandler.slots
			}

			override fun getStackInSlot(slot: Int): ItemStack {
				return currentHandler.getStackInSlot(slot)
			}

			override fun insertItem(slot: Int, stack: ItemStack, simulate: Boolean): ItemStack {
				return currentHandler.insertItem(slot, stack, simulate)
			}

			override fun extractItem(slot: Int, amount: Int, simulate: Boolean): ItemStack {
				return currentHandler.extractItem(slot, amount, simulate)
			}

			override fun getSlotLimit(slot: Int): Int {
				return currentHandler.getSlotLimit(slot)
			}

			override fun isItemValid(slot: Int, stack: ItemStack): Boolean {
				return currentHandler.isItemValid(slot, stack)
			}
		}
	}

	companion object {
		private fun determineDefaultMode(input: IItemHandler?, output: IItemHandler?, inputOutput: IItemHandler?, battery: IItemHandler?, side: RelativeSide): ItemHandlerMode {
			if (side == RelativeSide.BACK && battery != null) {
				return ItemHandlerMode.BATTERY
			}

			if (input == null && output == null && inputOutput == null) {
				return ItemHandlerMode.DISABLED
			} else if (inputOutput != null) {
				return ItemHandlerMode.INPUT_OUTPUT
			} else if (input != null && output != null) {
				return when (side) {
					RelativeSide.FRONT, RelativeSide.BACK -> ItemHandlerMode.DISABLED
					RelativeSide.RIGHT, RelativeSide.TOP -> ItemHandlerMode.INPUT
					RelativeSide.LEFT, RelativeSide.BOTTOM -> ItemHandlerMode.OUTPUT
				}
			} else if (output != null) {
				if (side == RelativeSide.TOP) {
					return ItemHandlerMode.DISABLED
				} else {
					return ItemHandlerMode.OUTPUT
				}
			} else {
				if (side == RelativeSide.BOTTOM) {
					return ItemHandlerMode.DISABLED
				} else {
					return ItemHandlerMode.INPUT
				}
			}
		}
	}
}
