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

import net.minecraft.core.BlockPos
import net.minecraft.nbt.StringTag
import net.minecraft.resources.ResourceLocation
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.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.state.BlockState
import net.minecraftforge.registries.ForgeRegistries
import ru.dbotthepony.mc.otm.block.entity.MatteryPoweredBlockEntity
import ru.dbotthepony.mc.otm.capability.FlowDirection
import ru.dbotthepony.mc.otm.capability.MatteryCapability
import ru.dbotthepony.mc.otm.capability.UpgradeType
import ru.dbotthepony.mc.otm.capability.energy.ProfiledEnergyStorage
import ru.dbotthepony.mc.otm.capability.energy.WorkerEnergyStorage
import ru.dbotthepony.mc.otm.capability.matter.IMatterStorage
import ru.dbotthepony.mc.otm.capability.matter.PatternState
import ru.dbotthepony.mc.otm.capability.matter.MatterStorageImpl
import ru.dbotthepony.mc.otm.capability.matter.ProfiledMatterStorage
import ru.dbotthepony.mc.otm.config.MachinesConfig
import ru.dbotthepony.mc.otm.container.HandlerFilter
import ru.dbotthepony.mc.otm.container.MatteryContainer
import ru.dbotthepony.mc.otm.container.UpgradeContainer
import ru.dbotthepony.mc.otm.core.math.Decimal
import ru.dbotthepony.mc.otm.core.registryName
import ru.dbotthepony.mc.otm.graph.matter.MatterNode
import ru.dbotthepony.mc.otm.matter.IMatterValue
import ru.dbotthepony.mc.otm.matter.MatterManager
import ru.dbotthepony.mc.otm.menu.matter.MatterReconstructorMenu
import ru.dbotthepony.mc.otm.registry.MBlockEntities

class MatterReconstructorBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryPoweredBlockEntity(MBlockEntities.MATTER_RECONSTRUCTOR, blockPos, blockState) {
	val repairContainer = MatteryContainer(::containerChanged, 1).also(::addDroppableContainer)

	private var matterPerTick = Decimal.ZERO
	private var progressPerTick = 0.0
	private var repairProgress = 0.0
	private var failureChance = 0.0
	private var lastItem: Item? = null
	private var initialDamage = 0.0

	var visualItemStack by synchronizer.item(observe = false)
		private set

	var visualProgress = 0f
		private set

	var isUnableToProcess = false
		private set

	val upgrades = UpgradeContainer(this::markDirtyFast, 3, UpgradeType.REPLICATOR)
	val matter = ProfiledMatterStorage(MatterStorageImpl(::markDirtyFast, FlowDirection.INPUT, upgrades.matterCapacity(MachinesConfig.MatterReconstructor.VALUES::matterCapacity)))
	override val energy = ProfiledEnergyStorage(WorkerEnergyStorage(::markDirtyFast, upgrades.transform(MachinesConfig.MatterReconstructor.VALUES)))

	val matterNode = object : MatterNode() {
		override fun getMatterHandler(): IMatterStorage {
			return matter
		}

		override fun onPatternAdded(state: PatternState) {
			containerChanged()
		}

		override fun onPatternRemoved(state: PatternState) {
			containerChanged()
		}

		override fun onPatternUpdated(newState: PatternState, oldState: PatternState) {
			containerChanged()
		}
	}

	init {
		exposeGlobally(MatteryCapability.MATTER, matter)
		exposeGlobally(MatteryCapability.MATTER_NODE, matterNode)

		savetables.stateful(::repairContainer)
		savetables.stateful(::matter)
		savetables.stateful(::energy)
		savetables.stateful(::upgrades)

		savetables.decimal(::matterPerTick)
		savetables.double(::progressPerTick)
		savetables.double(::repairProgress)

		savetables.Stateless(::lastItem, type = StringTag::class.java)
			.withSerializer { it?.registryName?.toString()?.let(StringTag::valueOf) }
			.withDeserializer { ResourceLocation.tryParse(it.asString)?.let { ForgeRegistries.ITEMS.getValue(it) } }
	}

	val energyConfig = ConfigurableEnergy(energy)
	val itemConfig = ConfigurableItemHandler(
		inputOutput = repairContainer.handler(object : HandlerFilter {
			override fun canInsert(slot: Int, stack: ItemStack): Boolean {
				if (!stack.isRepairable || !stack.isDamaged) {
					return false
				}

				if (MachinesConfig.MatterReconstructor.ALLOW_TO_SKIP_ANVIL && !MachinesConfig.MatterReconstructor.ONLY_ANVIL) {
					if (MatterManager.get(stack.item).hasMatterValue) {
						return true
					}
				}

				return matterNode.graph
					.patterns
					.filter { stack.item.isValidRepairItem(stack, ItemStack(it.item, 1)) }
					.findFirst().orElse(null).let {
						if (it == null) {
							IMatterValue.ZERO
						} else {
							MatterManager.get(it.item)
						}
					}.hasMatterValue
			}

			override fun canExtract(slot: Int, amount: Int, stack: ItemStack): Boolean {
				return progressPerTick <= 0.0 || matterPerTick <= Decimal.ZERO
			}
		})
	)

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

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

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

	private var changeset = 0

	private fun containerChanged() {
		matterPerTick = Decimal.ZERO
		progressPerTick = 0.0

		val item = repairContainer[0]
		val thisChangeset = ++changeset

		tickList.once {
			if (thisChangeset != changeset) return@once

			if (lastItem != item.item) {
				lastItem = item.item
				repairProgress = 0.0
				initialDamage = item.damageValue.toDouble()
				visualItemStack = item
				visualProgress = 0f
			}

			if (item.isEmpty || !item.isRepairable || !item.isDamaged) {
				matterPerTick = Decimal.ZERO
				progressPerTick = 0.0
				initialDamage = 0.0
				visualItemStack = item
				visualProgress = 0f
			} else {
				if (MachinesConfig.MatterReconstructor.ALLOW_TO_SKIP_ANVIL && !MachinesConfig.MatterReconstructor.ONLY_ANVIL) {
					val matter = MatterManager.get(item.item)

					if (matter.hasMatterValue) {
						failureChance = MachinesConfig.MatterReconstructor.FAILURE_CHANCE
						progressPerTick = (item.maxDamage / matter.complexity) / MachinesConfig.MatterReconstructor.DIVISOR
						matterPerTick = (matter.matter / matter.complexity) / MachinesConfig.MatterReconstructor.DIVISOR
						return@once
					}
				}

				val found = matterNode.graph.patterns.filter { item.item.isValidRepairItem(item, ItemStack(it.item, 1)) }.findFirst().orElse(null)

				if (found != null) {
					failureChance = (1.0 - found.researchPercent) + MachinesConfig.MatterReconstructor.FAILURE_CHANCE

					if (!MachinesConfig.MatterReconstructor.ONLY_ANVIL) {
						val matter = MatterManager.get(item.item)

						if (matter.hasMatterValue) {
							progressPerTick = (item.maxDamage / matter.complexity) / MachinesConfig.MatterReconstructor.DIVISOR
							matterPerTick = (matter.matter / matter.complexity) / MachinesConfig.MatterReconstructor.DIVISOR
							return@once
						}
					}

					@Suppress("name_shadowing")
					val matter = MatterManager.get(found.item) * 2

					progressPerTick = (item.maxDamage / matter.complexity) / MachinesConfig.MatterReconstructor.DIVISOR
					matterPerTick = (matter.matter / matter.complexity) / MachinesConfig.MatterReconstructor.DIVISOR
				} else {
					matterPerTick = Decimal.ZERO
					progressPerTick = 0.0
					initialDamage = 0.0
					visualItemStack = item
					visualProgress = 0f
				}
			}
		}
	}

	override fun tick() {
		super.tick()

		if (!redstoneControl.isBlockedByRedstone) {
			isUnableToProcess = false

			val item = repairContainer[0]

			val thisProgressPerTick = progressPerTick * (1.0 + upgrades.speedBonus)
			val matterPerTick = matterPerTick * (1.0 + upgrades.speedBonus)
			val energyConsumption = MachinesConfig.MatterReconstructor.VALUES.energyConsumption * (1.0 + upgrades.speedBonus) * (upgrades.energyConsumed + Decimal.ONE)

			if (!item.isEmpty && matterPerTick.isPositive && thisProgressPerTick > 0.0 && item.isRepairable && item.isDamaged) {
				var progressPerTick = (repairProgress + thisProgressPerTick).coerceAtMost(item.damageValue.toDouble()) - repairProgress
				if (progressPerTick <= 0.0) return

				if (energyConsumption.isPositive) {
					if (!energy.batteryLevel.isPositive) return
					val multEnergy = energyConsumption * (progressPerTick / thisProgressPerTick)
					progressPerTick *= (energy.extractEnergy(multEnergy, true) / multEnergy).toDouble()

					if (progressPerTick <= 0.0) {
						isUnableToProcess = true
						return
					}
				}

				if (matter.storedMatter < matterPerTick) {
					val toDrain = (matterPerTick * 200
						.coerceAtMost(item.damageValue) - matter.storedMatter)
						.coerceAtLeast(Decimal.ZERO)
						.coerceAtMost(matter.missingMatter)

					matter.receiveMatter(matterNode.graph.extractMatter(toDrain, false), false)
				}

				val toDrain = matterPerTick * (progressPerTick / thisProgressPerTick)
				val drain = matter.extractMatter(toDrain, true)

				if (!drain.isPositive) {
					isUnableToProcess = true
					return
				}

				progressPerTick *= (drain / toDrain).toDouble()

				if (progressPerTick <= 0.0) {
					isUnableToProcess = true
					return
				}

				if (failureChance * upgrades.failureMultiplier <= 0.0 || level!!.random.nextDouble() >= failureChance * upgrades.failureMultiplier)
					repairProgress += progressPerTick

				energy.extractEnergy(energyConsumption * (progressPerTick / thisProgressPerTick), false)
				matter.extractMatter(matterPerTick * (progressPerTick / thisProgressPerTick), false)

				if (repairProgress >= 1.0) {
					item.damageValue = (item.damageValue - repairProgress.toInt()).coerceAtLeast(0)
					repairProgress %= 1.0
				}

				visualItemStack = item
				visualProgress = 1f - ((item.damageValue - repairProgress) / initialDamage).toFloat()
			} else {
				initialDamage = 0.0
				visualItemStack = item
				visualProgress = 0f

				if (matter.storedMatter.isPositive) {
					matter.extractMatter(matterNode.graph.receiveMatter(matter.storedMatter, false), false)
				}
			}
		}
	}
}
