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

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.ItemStack
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.state.BlockState
import net.minecraftforge.common.ForgeConfigSpec
import ru.dbotthepony.mc.otm.block.entity.JobContainer
import ru.dbotthepony.mc.otm.block.entity.JobStatus
import ru.dbotthepony.mc.otm.block.entity.MachineJob
import ru.dbotthepony.mc.otm.block.entity.MatteryWorkerBlockEntity
import ru.dbotthepony.mc.otm.capability.FlowDirection
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.energy.BlockEnergyStorageImpl
import ru.dbotthepony.mc.otm.capability.energy.ProfiledEnergyStorage
import ru.dbotthepony.mc.otm.capability.energy.WorkerEnergyStorage
import ru.dbotthepony.mc.otm.capability.matter.MatterStorageImpl
import ru.dbotthepony.mc.otm.capability.matter.ProfiledMatterStorage
import ru.dbotthepony.mc.otm.config.ConciseBalanceValues
import ru.dbotthepony.mc.otm.container.HandlerFilter
import ru.dbotthepony.mc.otm.container.MatteryContainer
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.math.DecimalConfigValue
import ru.dbotthepony.mc.otm.core.math.defineDecimal
import ru.dbotthepony.mc.otm.core.math.getDecimal
import ru.dbotthepony.mc.otm.core.math.set
import ru.dbotthepony.mc.otm.core.nbt.set
import ru.dbotthepony.mc.otm.core.util.WriteOnce
import ru.dbotthepony.mc.otm.graph.matter.MatterGraph
import ru.dbotthepony.mc.otm.graph.matter.SimpleMatterNode
import ru.dbotthepony.mc.otm.item.matter.MatterDustItem
import ru.dbotthepony.mc.otm.matter.MatterManager
import ru.dbotthepony.mc.otm.menu.matter.MatterDecomposerMenu
import ru.dbotthepony.mc.otm.registry.MBlockEntities
import ru.dbotthepony.mc.otm.registry.MItems
import ru.dbotthepony.mc.otm.registry.MNames

fun moveMatterAsDustIntoContainer(_matterValue: Decimal, container: MatteryContainer, OUTPUT_DUST_MAIN: Int, OUTPUT_DUST_STACKING: Int): Decimal {
	var matterValue = _matterValue
	val item = MItems.MATTER_DUST as MatterDustItem

	while (matterValue > Decimal.ZERO) {
		val stack = container[OUTPUT_DUST_MAIN]

		// первый слот пустой
		if (stack.isEmpty) {
			container[OUTPUT_DUST_MAIN] = ItemStack(item, 1).also {
				matterValue -= item.addMatterValue(it, matterValue, false)
			}
			// первый слот не пустой, но мы можем влить туда материю
		} else if (!item.isFull(stack) && stack.count == 1) {
			matterValue -= item.addMatterValue(stack, matterValue, false)
			container.setChanged(OUTPUT_DUST_MAIN)
			// первый слот не пустой и мы не можем влить туда материю
		} else {
			val stack2 = container[OUTPUT_DUST_STACKING]

			// второй слот пустой
			if (stack2.isEmpty) {
				container[OUTPUT_DUST_STACKING] = ItemStack(item, 1).also {
					matterValue -= item.addMatterValue(it, matterValue, false)
				}
				// второй слот не пустой, но мы можем влить туда материю
			} else if (!item.isFull(stack2)) {
				if (stack2.count != 1) {
					return matterValue
				}

				matterValue -= item.addMatterValue(stack2, matterValue, false)
				container.setChanged(OUTPUT_DUST_STACKING)
			}

			// можем ли мы стакнуть материю из второго слота в первый?
			if (!stack2.isEmpty && item.isFull(stack2)) {
				if (ItemStack.isSameItemSameTags(stack, stack2) && container.getMaxStackSizeWithItem(OUTPUT_DUST_MAIN) >= stack.count + 1) {
					stack.count++
					stack2.count--

					container.setChanged(OUTPUT_DUST_MAIN)
					container.setChanged(OUTPUT_DUST_STACKING)
				} else {
					return matterValue
				}
			}
		}
	}

	return Decimal.ZERO
}

class MatterDecomposerBlockEntity(pos: BlockPos, state: BlockState)
	: MatteryWorkerBlockEntity<MatterDecomposerBlockEntity.DecomposerJob>(MBlockEntities.MATTER_DECOMPOSER, pos, state, ::DecomposerJob) {

	class DecomposerJob : MachineJob {
		val toDust: Boolean
		var matterValue: Decimal

		constructor(tag: CompoundTag) : super(tag) {
			toDust = tag.getBoolean(TO_DUST_KEY)
			matterValue = tag.getDecimal(MATTER_VALUE_KEY)
		}

		constructor(toDust: Boolean, matterValue: Decimal, ticks: Double) : super(ticks, BASE_CONSUMPTION) {
			this.toDust = toDust
			this.matterValue = matterValue
		}

		override fun serializeNBT(): CompoundTag {
			return super.serializeNBT().also {
				it[TO_DUST_KEY] = toDust
				it[MATTER_VALUE_KEY] = matterValue
			}
		}

		companion object {
			const val TO_DUST_KEY = "toDust"
			const val MATTER_VALUE_KEY = "matterValue"
		}
	}

	val energy = ProfiledEnergyStorage(WorkerEnergyStorage(this, ENERGY_VALUES))
	val energyConfig = ConfigurableEnergy(energy)

	init {
		savetables.stateful(::energy, ENERGY_KEY)
	}

	val matter = ProfiledMatterStorage(MatterStorageImpl(::setChangedLight, FlowDirection.OUTPUT, ::CAPACITY))
	val matterNode = SimpleMatterNode(matter = matter)

	init {
		exposeGlobally(MatteryCapability.MATTER, matter)
		exposeGlobally(MatteryCapability.MATTER_NODE, matterNode)
		savetables.stateful(::matter, MATTER_STORAGE_KEY)
	}

	// вход, выход
	val inputContainer = MatteryContainer(::setChangedLight, 1).also(::addDroppableContainer)
	val outputContainer = MatteryContainer(::setChangedLight, 2).also(::addDroppableContainer)

	val itemConfig = ConfigurableItemHandler(
		input = inputContainer.handler(object : HandlerFilter {
			override fun canInsert(slot: Int, stack: ItemStack): Boolean {
				return MatterManager.canDecompose(stack)
			}

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

		output = outputContainer.handler(HandlerFilter.OnlyOut)
	)

	init {
		savetables.stateful(::inputContainer)
		savetables.stateful(::outputContainer)
	}

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

	override fun onJobFinish(job: DecomposerJob, id: Int): JobStatus {
		if (job.toDust) {
			job.matterValue = moveMatterAsDustIntoContainer(job.matterValue, outputContainer, 0, 1)

			if (!job.matterValue.isZero)
				return JobStatus.FAILURE_WAIT_FAST

			return JobStatus.SUCCESS
		}

		job.matterValue -= matter.receiveMatter(job.matterValue, false)

		if (job.matterValue.isPositive)
			return JobStatus.FAILURE_MATTER

		return JobStatus.SUCCESS
	}

	override fun computeNextJob(id: Int): JobContainer<DecomposerJob> {
		val stack = inputContainer[0]

		if (!stack.isEmpty) {
			val copy = stack.copy()
			copy.count = 1

			if (MatterManager.canDecompose(copy)) {
				val matter = MatterManager.get(copy)
				stack.count--

				return JobContainer.success(DecomposerJob((level?.random?.nextDouble() ?: 1.0) <= 0.2, matter.matter, matter.complexity))
			}
		}

		return JobContainer.noItem()
	}

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

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

	override fun tick() {
		super.tick()

		val grid = matterNode.graph as MatterGraph? ?: return

		if (!matter.storedMatter.isZero) {
			val diff = matter.extractMatter(matter.storedMatter, true)
			val diff2 = grid.receiveMatter(diff, true)

			matter.extractMatter(diff2, false)
			grid.receiveMatter(diff2, false)
		}
	}

	companion object {
		val CAPACITY get() = _CAPACITY.get()
		val BASE_CONSUMPTION get() = _BASE_CONSUMPTION.get()

		private var _CAPACITY: DecimalConfigValue by WriteOnce()
		private var _BASE_CONSUMPTION: DecimalConfigValue by WriteOnce()

		var ENERGY_VALUES: ConciseBalanceValues by WriteOnce()
			private set

		fun registerConfig(builder: ForgeConfigSpec.Builder) {
			builder.push(MNames.MATTER_DECOMPOSER)

			ENERGY_VALUES = BlockEnergyStorageImpl.makeConfigEntry(builder, capacity = Decimal(400_000), throughput = Decimal(2_000))

			_CAPACITY = builder.defineDecimal("matterCapacity", Decimal(500), Decimal.ONE_TENTH)
			_BASE_CONSUMPTION = builder.defineDecimal("baseConsumption", Decimal(240), Decimal.ONE)

			builder.pop()
		}
	}
}
